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    A2AAgentCard, AgentSkill, CardCredentials, SelfProtocolExtension, TrustModel,
15    get_provider_label,
16};
17use crate::constants::{
18    headers, network_config, IAgentRegistry, IHumanProofProvider, NetworkName, DEFAULT_NETWORK,
19};
20use crate::registration_flow::{
21    DeregistrationRequest, DeregistrationSession, RegistrationError, RegistrationRequest,
22    RegistrationSession, DEFAULT_API_BASE,
23};
24
25/// Configuration for creating a [`SelfAgent`].
26#[derive(Debug, Clone)]
27pub struct SelfAgentConfig {
28    /// Agent's private key (hex, with or without 0x prefix).
29    pub private_key: String,
30    /// Network to use: Mainnet (default) or Testnet.
31    pub network: Option<NetworkName>,
32    /// Override: custom registry address (takes precedence over network).
33    pub registry_address: Option<Address>,
34    /// Override: custom RPC URL (takes precedence over network).
35    pub rpc_url: Option<String>,
36}
37
38/// Full agent info from the registry.
39#[derive(Debug, Clone)]
40pub struct AgentInfo {
41    pub address: Address,
42    pub agent_key: B256,
43    pub agent_id: U256,
44    pub is_verified: bool,
45    pub nullifier: U256,
46    pub agent_count: U256,
47}
48
49/// Agent-side SDK for Self Agent ID.
50///
51/// The agent's on-chain identity is its Ethereum address, zero-padded to bytes32.
52/// For off-chain authentication, the agent signs each request with its private key.
53pub struct SelfAgent {
54    signer: PrivateKeySigner,
55    private_key: String,
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            private_key: config.private_key,
77            network_name,
78            registry_address: config.registry_address.unwrap_or(net.registry_address),
79            rpc_url: config.rpc_url.unwrap_or_else(|| net.rpc_url.to_string()),
80            agent_key,
81            http_client: Client::new(),
82        })
83    }
84
85    /// The agent's Ethereum address.
86    pub fn address(&self) -> Address {
87        self.signer.address()
88    }
89
90    /// The agent's on-chain key (bytes32) — zero-padded address.
91    pub fn agent_key(&self) -> B256 {
92        self.agent_key
93    }
94
95    fn make_provider(
96        &self,
97    ) -> Result<impl alloy::providers::Provider + Clone, crate::Error> {
98        let url: reqwest::Url = self
99            .rpc_url
100            .parse()
101            .map_err(|_| crate::Error::InvalidRpcUrl)?;
102        Ok(ProviderBuilder::new().connect_http(url))
103    }
104
105    /// Create a provider with wallet attached — required for write operations.
106    fn make_signer_provider(
107        &self,
108    ) -> Result<impl alloy::providers::Provider + Clone, crate::Error> {
109        let url: reqwest::Url = self
110            .rpc_url
111            .parse()
112            .map_err(|_| crate::Error::InvalidRpcUrl)?;
113        let wallet = alloy::network::EthereumWallet::from(self.signer.clone());
114        Ok(ProviderBuilder::new().wallet(wallet).connect_http(url))
115    }
116
117    /// Check if this agent is registered and verified on-chain.
118    pub async fn is_registered(&self) -> Result<bool, crate::Error> {
119        let provider = self.make_provider()?;
120        let registry = IAgentRegistry::new(self.registry_address, provider);
121        let result = registry
122            .isVerifiedAgent(self.agent_key)
123            .call()
124            .await
125            .map_err(|e| crate::Error::RpcError(e.to_string()))?;
126        Ok(result)
127    }
128
129    /// Get full agent info from the registry.
130    pub async fn get_info(&self) -> Result<AgentInfo, crate::Error> {
131        let provider = self.make_provider()?;
132        let registry = IAgentRegistry::new(self.registry_address, provider);
133
134        let agent_id = registry
135            .getAgentId(self.agent_key)
136            .call()
137            .await
138            .map_err(|e| crate::Error::RpcError(e.to_string()))?;
139
140        if agent_id == U256::ZERO {
141            return Ok(AgentInfo {
142                address: self.signer.address(),
143                agent_key: self.agent_key,
144                agent_id: U256::ZERO,
145                is_verified: false,
146                nullifier: U256::ZERO,
147                agent_count: U256::ZERO,
148            });
149        }
150
151        let is_verified = registry
152            .hasHumanProof(agent_id)
153            .call()
154            .await
155            .map_err(|e| crate::Error::RpcError(e.to_string()))?;
156        let nullifier = registry
157            .getHumanNullifier(agent_id)
158            .call()
159            .await
160            .map_err(|e| crate::Error::RpcError(e.to_string()))?;
161        let agent_count = registry
162            .getAgentCountForHuman(nullifier)
163            .call()
164            .await
165            .map_err(|e| crate::Error::RpcError(e.to_string()))?;
166
167        Ok(AgentInfo {
168            address: self.signer.address(),
169            agent_key: self.agent_key,
170            agent_id,
171            is_verified,
172            nullifier,
173            agent_count,
174        })
175    }
176
177    /// Generate authentication headers for a request.
178    ///
179    /// Signature covers: `keccak256(timestamp + METHOD + canonicalPathAndQuery + bodyHash)`
180    pub async fn sign_request(
181        &self,
182        method: &str,
183        url: &str,
184        body: Option<&str>,
185    ) -> Result<HashMap<String, String>, crate::Error> {
186        let timestamp = now_millis().to_string();
187        self.sign_request_with_timestamp(method, url, body, &timestamp)
188            .await
189    }
190
191    /// Sign a request with a specific timestamp (useful for testing).
192    pub async fn sign_request_with_timestamp(
193        &self,
194        method: &str,
195        url: &str,
196        body: Option<&str>,
197        timestamp: &str,
198    ) -> Result<HashMap<String, String>, crate::Error> {
199        let message = compute_signing_message(timestamp, method, url, body);
200
201        // EIP-191 personal_sign over the raw 32 bytes
202        let signature = self
203            .signer
204            .sign_message(message.as_ref())
205            .await
206            .map_err(|e| crate::Error::SigningError(e.to_string()))?;
207
208        let sig_hex = format!("0x{}", hex::encode(signature.as_bytes()));
209
210        let mut headers_map = HashMap::new();
211        headers_map.insert(
212            headers::ADDRESS.to_string(),
213            format!("{:#x}", self.signer.address()),
214        );
215        headers_map.insert(headers::SIGNATURE.to_string(), sig_hex);
216        headers_map.insert(headers::TIMESTAMP.to_string(), timestamp.to_string());
217
218        Ok(headers_map)
219    }
220
221    /// Wrapper around reqwest that automatically adds agent signature headers.
222    pub async fn fetch(
223        &self,
224        url: &str,
225        method: Option<Method>,
226        body: Option<String>,
227    ) -> Result<Response, crate::Error> {
228        let method = method.unwrap_or(Method::GET);
229        let method_str = method.as_str();
230        let body_ref = body.as_deref();
231
232        let auth_headers = self.sign_request(method_str, url, body_ref).await?;
233
234        let mut request: RequestBuilder = self.http_client.request(method, url);
235        for (k, v) in &auth_headers {
236            request = request.header(k, v);
237        }
238        if let Some(b) = body {
239            request = request.header("content-type", "application/json");
240            request = request.body(b);
241        }
242
243        request
244            .send()
245            .await
246            .map_err(|e| crate::Error::HttpError(e.to_string()))
247    }
248
249    // ─── A2A Agent Card Methods ────────────────────────────────────────────
250
251    /// Read the A2A Agent Card from on-chain metadata (if set).
252    pub async fn get_agent_card(&self) -> Result<Option<A2AAgentCard>, 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::<A2AAgentCard>(&raw) {
275            Ok(card) if card.a2a_version == "0.1" => 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        let card = A2AAgentCard {
364            a2a_version: "0.1".into(),
365            name,
366            description,
367            url,
368            capabilities: None,
369            skills,
370            self_protocol: SelfProtocolExtension {
371                agent_id: agent_id.try_into().unwrap_or(0),
372                registry: format!("{:#x}", self.registry_address),
373                chain_id,
374                proof_provider: format!("{:#x}", proof_provider_addr),
375                provider_name,
376                verification_strength: strength,
377                trust_model,
378                credentials: card_credentials,
379            },
380        };
381
382        let json =
383            serde_json::to_string(&card).map_err(|e| crate::Error::RpcError(e.to_string()))?;
384
385        let tx_hash = registry
386            .updateAgentMetadata(agent_id, json)
387            .send()
388            .await
389            .map_err(|e| crate::Error::RpcError(e.to_string()))?
390            .watch()
391            .await
392            .map_err(|e| crate::Error::RpcError(e.to_string()))?;
393
394        Ok(tx_hash)
395    }
396
397    /// Returns a `data:` URI containing the base64-encoded Agent Card JSON.
398    pub async fn to_agent_card_data_uri(&self) -> Result<String, crate::Error> {
399        let card = self
400            .get_agent_card()
401            .await?
402            .ok_or_else(|| crate::Error::RpcError("No A2A Agent Card set".into()))?;
403        let json =
404            serde_json::to_string(&card).map_err(|e| crate::Error::RpcError(e.to_string()))?;
405        use base64::Engine;
406        let encoded = base64::engine::general_purpose::STANDARD.encode(json.as_bytes());
407        Ok(format!("data:application/json;base64,{}", encoded))
408    }
409
410    /// Read ZK-attested credentials for this agent from on-chain.
411    pub async fn get_credentials(
412        &self,
413    ) -> Result<Option<IAgentRegistry::AgentCredentials>, crate::Error> {
414        let provider = self.make_provider()?;
415        let registry = IAgentRegistry::new(self.registry_address, provider);
416
417        let agent_id = registry
418            .getAgentId(self.agent_key)
419            .call()
420            .await
421            .map_err(|e| crate::Error::RpcError(e.to_string()))?;
422        if agent_id == U256::ZERO {
423            return Ok(None);
424        }
425
426        let creds = registry
427            .getAgentCredentials(agent_id)
428            .call()
429            .await
430            .map_err(|e| crate::Error::RpcError(e.to_string()))?;
431        Ok(Some(creds))
432    }
433
434    /// Read the verification strength score from the provider contract.
435    pub async fn get_verification_strength(&self) -> Result<u8, crate::Error> {
436        let provider = self.make_provider()?;
437        let registry = IAgentRegistry::new(self.registry_address, &provider);
438
439        let agent_id = registry
440            .getAgentId(self.agent_key)
441            .call()
442            .await
443            .map_err(|e| crate::Error::RpcError(e.to_string()))?;
444        if agent_id == U256::ZERO {
445            return Ok(0);
446        }
447
448        let provider_addr = registry
449            .getProofProvider(agent_id)
450            .call()
451            .await
452            .map_err(|e| crate::Error::RpcError(e.to_string()))?;
453        if provider_addr == Address::ZERO {
454            return Ok(0);
455        }
456
457        let proof_provider = IHumanProofProvider::new(provider_addr, &provider);
458        let strength = proof_provider
459            .verificationStrength()
460            .call()
461            .await
462            .map_err(|e| crate::Error::RpcError(e.to_string()))?;
463        Ok(strength)
464    }
465
466    // ─── Registration / Deregistration (REST API) ────────────────────────
467
468    /// Initiate agent registration via the REST API.
469    ///
470    /// Returns a [`RegistrationSession`] with a QR code URL and deep link
471    /// that the human operator must scan with the Self app.
472    pub async fn request_registration(
473        req: RegistrationRequest,
474        api_base: Option<&str>,
475    ) -> Result<RegistrationSession, RegistrationError> {
476        RegistrationSession::request(req, api_base).await
477    }
478
479    /// Query agent info via the REST API (no private key needed).
480    pub async fn get_agent_info_rest(
481        agent_id: u64,
482        network: NetworkName,
483        api_base: Option<&str>,
484    ) -> Result<serde_json::Value, RegistrationError> {
485        let base = api_base.unwrap_or(DEFAULT_API_BASE);
486        let chain_id: u64 = match network {
487            NetworkName::Mainnet => 42220,
488            NetworkName::Testnet => 11142220,
489        };
490        let resp = reqwest::get(format!("{base}/api/agent/info/{chain_id}/{agent_id}"))
491            .await
492            .map_err(|e| RegistrationError::Http(e.to_string()))?;
493        resp.json()
494            .await
495            .map_err(|e| RegistrationError::Http(e.to_string()))
496    }
497
498    /// Query all agents registered to a human address via the REST API.
499    pub async fn get_agents_for_human(
500        address: &str,
501        network: NetworkName,
502        api_base: Option<&str>,
503    ) -> Result<serde_json::Value, RegistrationError> {
504        let base = api_base.unwrap_or(DEFAULT_API_BASE);
505        let chain_id: u64 = match network {
506            NetworkName::Mainnet => 42220,
507            NetworkName::Testnet => 11142220,
508        };
509        let resp = reqwest::get(format!("{base}/api/agent/agents/{chain_id}/{address}"))
510            .await
511            .map_err(|e| RegistrationError::Http(e.to_string()))?;
512        resp.json()
513            .await
514            .map_err(|e| RegistrationError::Http(e.to_string()))
515    }
516
517    /// Initiate deregistration for this agent via the REST API.
518    ///
519    /// Returns a [`DeregistrationSession`] with a QR code URL that the
520    /// human operator must scan with the Self app to confirm removal.
521    pub async fn request_deregistration(
522        &self,
523        api_base: Option<&str>,
524    ) -> Result<DeregistrationSession, RegistrationError> {
525        let network_str = match self.network_name {
526            NetworkName::Mainnet => "mainnet",
527            NetworkName::Testnet => "testnet",
528        };
529        DeregistrationSession::request(
530            DeregistrationRequest {
531                network: network_str.to_string(),
532                agent_address: format!("{:#x}", self.signer.address()),
533            },
534            api_base,
535        )
536        .await
537    }
538}
539
540/// Convert a 20-byte address to a 32-byte agent key (left zero-padded).
541/// Matches TS: `ethers.zeroPadValue(address, 32)`
542pub fn address_to_agent_key(address: Address) -> B256 {
543    let mut bytes = [0u8; 32];
544    bytes[12..32].copy_from_slice(address.as_ref());
545    FixedBytes(bytes)
546}
547
548/// Current time in milliseconds since Unix epoch.
549fn now_millis() -> u64 {
550    SystemTime::now()
551        .duration_since(UNIX_EPOCH)
552        .unwrap()
553        .as_millis() as u64
554}
555
556/// Returns `Some(s)` if non-empty, `None` otherwise.
557fn non_empty(s: &str) -> Option<String> {
558    if s.is_empty() {
559        None
560    } else {
561        Some(s.to_string())
562    }
563}
564
565/// Compute the signing message from request components.
566/// Exposed for use by the verifier.
567pub(crate) fn compute_signing_message(
568    timestamp: &str,
569    method: &str,
570    url: &str,
571    body: Option<&str>,
572) -> B256 {
573    let canonical_url = canonicalize_signing_url(url);
574    let body_text = body.unwrap_or("");
575    let body_hash = keccak256(body_text.as_bytes());
576    let body_hash_hex = format!("{:#x}", body_hash);
577    let concat = format!(
578        "{}{}{}{}",
579        timestamp,
580        method.to_uppercase(),
581        canonical_url,
582        body_hash_hex
583    );
584    keccak256(concat.as_bytes())
585}
586
587/// Canonical URL for signing/verification: path + optional query string.
588pub(crate) fn canonicalize_signing_url(url: &str) -> String {
589    if url.is_empty() {
590        return String::new();
591    }
592
593    if url.starts_with("http://") || url.starts_with("https://") {
594        if let Ok(parsed) = reqwest::Url::parse(url) {
595            let mut out = parsed.path().to_string();
596            if out.is_empty() {
597                out.push('/');
598            }
599            if let Some(query) = parsed.query() {
600                out.push('?');
601                out.push_str(query);
602            }
603            return out;
604        }
605        return url.to_string();
606    }
607
608    if url.starts_with('?') {
609        return format!("/{url}");
610    }
611    if url.starts_with('/') {
612        return url.to_string();
613    }
614
615    // Best effort for inputs like "api/data?x=1"
616    if let Ok(parsed) = reqwest::Url::parse(&format!("http://self.local/{url}")) {
617        let mut out = parsed.path().to_string();
618        if let Some(query) = parsed.query() {
619            out.push('?');
620            out.push_str(query);
621        }
622        return out;
623    }
624
625    url.to_string()
626}