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    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    /// Create a new agent instance.
66    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    /// The agent's Ethereum address.
87    pub fn address(&self) -> Address {
88        self.signer.address()
89    }
90
91    /// The agent's on-chain key (bytes32) — zero-padded address.
92    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    /// Create a provider with wallet attached — required for write operations.
107    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    /// Check if this agent is registered and verified on-chain.
119    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    /// Get full agent info from the registry.
131    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    /// Generate authentication headers for a request.
179    ///
180    /// Signature covers: `keccak256(timestamp + METHOD + canonicalPathAndQuery + bodyHash)`
181    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, &timestamp)
189            .await
190    }
191
192    /// Sign a request with a specific timestamp (useful for testing).
193    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        // EIP-191 personal_sign over the raw 32 bytes
203        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    /// Wrapper around reqwest that automatically adds agent signature headers.
223    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    // ─── A2A Agent Card Methods ────────────────────────────────────────────
251
252    /// Read the agent card from on-chain metadata (if set).
253    /// Supports both the new ERC-8004 format and legacy A2A v0.1 cards.
254    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    /// Build and write an A2A Agent Card to on-chain metadata.
283    /// Returns the transaction hash.
284    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        // Build services and supportedInterfaces if a URL is provided
366        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    /// Returns a `data:` URI containing the base64-encoded Agent Card JSON.
432    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    /// Read ZK-attested credentials for this agent from on-chain.
445    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    /// Read the verification strength score from the provider contract.
469    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    // ─── Registration / Deregistration (REST API) ────────────────────────
501
502    /// Initiate agent registration via the REST API.
503    ///
504    /// Returns a [`RegistrationSession`] with a QR code URL and deep link
505    /// that the human operator must scan with the Self app.
506    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    /// Query agent info via the REST API (no private key needed).
514    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    /// Query all agents registered to a human address via the REST API.
533    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    /// Initiate deregistration for this agent via the REST API.
552    ///
553    /// Returns a [`DeregistrationSession`] with a QR code URL that the
554    /// human operator must scan with the Self app to confirm removal.
555    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
574/// Convert a 20-byte address to a 32-byte agent key (left zero-padded).
575/// Matches TS: `ethers.zeroPadValue(address, 32)`
576pub 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
582/// Current time in milliseconds since Unix epoch.
583fn now_millis() -> u64 {
584    SystemTime::now()
585        .duration_since(UNIX_EPOCH)
586        .unwrap()
587        .as_millis() as u64
588}
589
590/// Returns `Some(s)` if non-empty, `None` otherwise.
591fn non_empty(s: &str) -> Option<String> {
592    if s.is_empty() {
593        None
594    } else {
595        Some(s.to_string())
596    }
597}
598
599/// Compute the signing message from request components.
600/// Exposed for use by the verifier.
601pub(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
621/// Canonical URL for signing/verification: path + optional query string.
622pub(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    // Best effort for inputs like "api/data?x=1"
650    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}