1use alloy::primitives::Address;
20
21#[cfg(feature = "erc8004")]
25pub mod contracts;
26#[cfg(feature = "erc8004")]
28pub mod deploy;
29#[cfg(feature = "erc8004")]
31pub mod discovery;
32#[cfg(feature = "erc8004")]
34pub mod onchain;
35#[cfg(feature = "erc8004")]
37pub mod recovery;
38#[cfg(feature = "erc8004")]
40pub mod reputation;
41#[cfg(feature = "erc8004")]
43pub mod types;
44#[cfg(feature = "erc8004")]
46pub mod validation;
47
48use alloy::signers::local::PrivateKeySigner;
50use chrono::{DateTime, Utc};
51#[cfg(feature = "erc8004")]
52pub use discovery::PeerInfo;
53use serde::{Deserialize, Serialize};
54use std::env;
55use std::path::Path;
56#[cfg(feature = "erc8004")]
57pub use types::{AgentId, AgentMetadata, ReputationScore};
58
59#[derive(Clone, Debug, Serialize, Deserialize)]
61pub struct InstanceIdentity {
62 #[serde(skip_serializing)]
64 pub private_key: String,
65 pub address: Address,
67 pub instance_id: String,
69 pub parent_url: Option<String>,
71 pub parent_address: Option<Address>,
73 pub created_at: DateTime<Utc>,
75 #[serde(default)]
77 pub agent_token_id: Option<String>,
78 #[serde(skip_serializing, default)]
82 pub facilitator_private_key: Option<String>,
83}
84
85#[derive(Serialize, Deserialize)]
87struct PersistedIdentity {
88 private_key: String,
89 address: String,
90 instance_id: String,
91 parent_url: Option<String>,
92 parent_address: Option<String>,
93 created_at: String,
94 #[serde(default)]
96 agent_token_id: Option<String>,
97 #[serde(default)]
99 facilitator_private_key: Option<String>,
100}
101
102impl From<&InstanceIdentity> for PersistedIdentity {
103 fn from(id: &InstanceIdentity) -> Self {
104 Self {
105 private_key: id.private_key.clone(),
106 address: format!("{:#x}", id.address),
107 instance_id: id.instance_id.clone(),
108 parent_url: id.parent_url.clone(),
109 parent_address: id.parent_address.map(|a| format!("{:#x}", a)),
110 created_at: id.created_at.to_rfc3339(),
111 agent_token_id: id.agent_token_id.clone(),
112 facilitator_private_key: id.facilitator_private_key.clone(),
113 }
114 }
115}
116
117impl TryFrom<PersistedIdentity> for InstanceIdentity {
118 type Error = IdentityError;
119
120 fn try_from(p: PersistedIdentity) -> Result<Self, Self::Error> {
121 let address: Address = p
122 .address
123 .parse()
124 .map_err(|e| IdentityError::ParseError(format!("invalid address: {e}")))?;
125 let parent_address = p
126 .parent_address
127 .map(|a| {
128 a.parse::<Address>()
129 .map_err(|e| IdentityError::ParseError(format!("invalid parent address: {e}")))
130 })
131 .transpose()?;
132 let created_at = DateTime::parse_from_rfc3339(&p.created_at)
133 .map(|dt| dt.with_timezone(&Utc))
134 .map_err(|e| IdentityError::ParseError(format!("invalid created_at: {e}")))?;
135
136 Ok(InstanceIdentity {
137 private_key: p.private_key,
138 address,
139 instance_id: p.instance_id,
140 parent_url: p.parent_url,
141 parent_address,
142 created_at,
143 agent_token_id: p.agent_token_id,
144 facilitator_private_key: p.facilitator_private_key,
145 })
146 }
147}
148
149#[derive(Debug, thiserror::Error)]
150pub enum IdentityError {
151 #[error("I/O error: {0}")]
152 IoError(#[from] std::io::Error),
153
154 #[error("parse error: {0}")]
155 ParseError(String),
156
157 #[error("faucet error: {0}")]
158 FaucetError(String),
159
160 #[error("registration error: {0}")]
161 RegistrationError(String),
162}
163
164pub fn bootstrap(identity_path: &str) -> Result<InstanceIdentity, IdentityError> {
172 let path = Path::new(identity_path);
173
174 let mut identity = if path.exists() {
175 tracing::info!("Loading existing identity from {}", identity_path);
176 let data = std::fs::read_to_string(path)?;
177 let persisted: PersistedIdentity = serde_json::from_str(&data)
178 .map_err(|e| IdentityError::ParseError(format!("invalid identity JSON: {e}")))?;
179 InstanceIdentity::try_from(persisted)?
180 } else {
181 tracing::info!("Generating new identity at {}", identity_path);
182 let signer = PrivateKeySigner::random();
183 let private_key = format!("0x{}", alloy::hex::encode(signer.to_bytes()));
184 let address = signer.address();
185
186 let instance_id = env::var("INSTANCE_ID")
187 .or_else(|_| env::var("RAILWAY_SERVICE_ID"))
188 .unwrap_or_else(|_| uuid::Uuid::new_v4().to_string());
189
190 let parent_url = env::var("PARENT_URL").ok().filter(|s| !s.is_empty());
191
192 if let Some(ref url) = parent_url {
194 if !url.starts_with("https://") {
195 tracing::warn!(
196 "PARENT_URL must use HTTPS — ignoring insecure value: {}",
197 &url[..url.len().min(20)]
198 );
199 }
201 }
202 let parent_url = parent_url.filter(|u| u.starts_with("https://"));
203 let parent_address = env::var("PARENT_ADDRESS")
204 .ok()
205 .and_then(|s| s.parse::<Address>().ok());
206
207 let fac_signer = PrivateKeySigner::random();
211 let facilitator_private_key = format!("0x{}", alloy::hex::encode(fac_signer.to_bytes()));
212
213 let identity = InstanceIdentity {
214 private_key,
215 address,
216 instance_id,
217 parent_url,
218 parent_address,
219 created_at: Utc::now(),
220 agent_token_id: None,
221 facilitator_private_key: Some(facilitator_private_key),
222 };
223
224 if let Some(parent_dir) = path.parent() {
226 std::fs::create_dir_all(parent_dir)?;
227 }
228
229 let persisted = PersistedIdentity::from(&identity);
231 let json = serde_json::to_string_pretty(&persisted)
232 .map_err(|e| IdentityError::ParseError(format!("serialize failed: {e}")))?;
233 std::fs::write(path, json)?;
234
235 #[cfg(unix)]
237 {
238 use std::os::unix::fs::PermissionsExt;
239 std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))?;
240 }
241
242 tracing::info!("New identity created: {:#x}", address);
243 identity
244 };
245
246 if identity.facilitator_private_key.is_none() {
248 tracing::info!("Generating separate facilitator key for existing identity");
249 let fac_signer = PrivateKeySigner::random();
250 identity.facilitator_private_key =
251 Some(format!("0x{}", alloy::hex::encode(fac_signer.to_bytes())));
252
253 let persisted = PersistedIdentity::from(&identity);
255 let json = serde_json::to_string_pretty(&persisted)
256 .map_err(|e| IdentityError::ParseError(format!("serialize failed: {e}")))?;
257 std::fs::write(path, json)?;
258 tracing::info!("Identity updated with separate facilitator key");
259 }
260
261 inject_env_vars(&identity);
263
264 Ok(identity)
265}
266
267fn inject_env_vars(identity: &InstanceIdentity) {
272 let address_str = format!("{:#x}", identity.address);
273
274 if env::var("EVM_ADDRESS").is_err() {
275 env::set_var("EVM_ADDRESS", &address_str);
276 tracing::debug!("Injected EVM_ADDRESS={}", address_str);
277 }
278
279 let fac_key = identity
282 .facilitator_private_key
283 .as_deref()
284 .unwrap_or(&identity.private_key);
285 env::set_var("FACILITATOR_PRIVATE_KEY", fac_key);
286 tracing::debug!("Injected FACILITATOR_PRIVATE_KEY (per-node)");
287
288 if env::var("EVM_PRIVATE_KEY").is_err() {
291 env::set_var("EVM_PRIVATE_KEY", &identity.private_key);
292 tracing::debug!("Injected EVM_PRIVATE_KEY");
293 }
294
295 if env::var("FACILITATOR_SHARED_SECRET").is_err() {
296 let secret = x402::hmac::compute_hmac(fac_key.as_bytes(), b"x402-bootstrap-hmac-secret");
300 env::set_var("FACILITATOR_SHARED_SECRET", &secret);
301 tracing::debug!("Injected FACILITATOR_SHARED_SECRET (auto-generated)");
302 }
303}
304
305pub fn save_agent_token_id(
309 identity_path: &str,
310 identity: &mut InstanceIdentity,
311 token_id: &str,
312) -> Result<(), IdentityError> {
313 identity.agent_token_id = Some(token_id.to_string());
314 let persisted = PersistedIdentity::from(&*identity);
315 let json = serde_json::to_string_pretty(&persisted)
316 .map_err(|e| IdentityError::ParseError(format!("serialize failed: {e}")))?;
317 std::fs::write(identity_path, json)?;
318 Ok(())
319}
320
321pub async fn request_faucet_funds(rpc_url: &str, address: Address) -> Result<(), IdentityError> {
326 let client = reqwest::Client::builder()
327 .redirect(reqwest::redirect::Policy::none())
328 .build()
329 .expect("failed to create HTTP client");
330 let address_str = format!("{:#x}", address);
331
332 let body = serde_json::json!({
333 "jsonrpc": "2.0",
334 "method": "tempo_fundAddress",
335 "params": [address_str],
336 "id": 1
337 });
338
339 let mut last_err = String::new();
340 for attempt in 0..3 {
341 if attempt > 0 {
342 let delay = std::time::Duration::from_secs(2u64.pow(attempt));
343 tokio::time::sleep(delay).await;
344 }
345
346 match client
347 .post(rpc_url)
348 .header("Content-Type", "application/json")
349 .json(&body)
350 .send()
351 .await
352 {
353 Ok(resp) if resp.status().is_success() => {
354 tracing::info!("Faucet funding requested for {}", address_str);
355 return Ok(());
356 }
357 Ok(resp) => {
358 last_err = format!("HTTP {}", resp.status());
359 tracing::warn!(
360 "Faucet request attempt {} failed: {}",
361 attempt + 1,
362 last_err
363 );
364 }
365 Err(e) => {
366 last_err = e.to_string();
367 tracing::warn!(
368 "Faucet request attempt {} failed: {}",
369 attempt + 1,
370 last_err
371 );
372 }
373 }
374 }
375
376 Err(IdentityError::FaucetError(format!(
377 "faucet funding failed after 3 attempts: {}",
378 last_err
379 )))
380}
381
382pub async fn register_with_parent(
386 parent_url: &str,
387 identity: &InstanceIdentity,
388 self_url: &str,
389) -> Result<(), IdentityError> {
390 let client = reqwest::Client::builder()
391 .redirect(reqwest::redirect::Policy::none())
392 .build()
393 .expect("failed to create HTTP client");
394 let url = format!("{}/instance/register", parent_url.trim_end_matches('/'));
395
396 let body = serde_json::json!({
397 "instance_id": identity.instance_id,
398 "address": format!("{:#x}", identity.address),
399 "url": self_url,
400 });
401
402 let mut last_err = String::new();
403 for attempt in 0..5 {
404 if attempt > 0 {
405 let delay = std::time::Duration::from_secs(2u64.pow(attempt));
406 tokio::time::sleep(delay).await;
407 }
408
409 match client
410 .post(&url)
411 .header("Content-Type", "application/json")
412 .json(&body)
413 .send()
414 .await
415 {
416 Ok(resp) if resp.status().is_success() => {
417 tracing::info!("Registered with parent at {}", parent_url);
418 return Ok(());
419 }
420 Ok(resp) => {
421 last_err = format!("HTTP {}", resp.status());
422 tracing::warn!(
423 "Parent registration attempt {} failed: {}",
424 attempt + 1,
425 last_err
426 );
427 }
428 Err(e) => {
429 last_err = e.to_string();
430 tracing::warn!(
431 "Parent registration attempt {} failed: {}",
432 attempt + 1,
433 last_err
434 );
435 }
436 }
437 }
438
439 Err(IdentityError::RegistrationError(format!(
440 "parent registration failed after 5 attempts: {}",
441 last_err
442 )))
443}
444
445#[cfg(feature = "erc8004")]
451pub fn identity_registry() -> Address {
452 std::env::var("ERC8004_IDENTITY_REGISTRY")
453 .ok()
454 .and_then(|s| s.parse().ok())
455 .unwrap_or(Address::ZERO)
456}
457
458#[cfg(feature = "erc8004")]
460pub fn reputation_registry() -> Address {
461 std::env::var("ERC8004_REPUTATION_REGISTRY")
462 .ok()
463 .and_then(|s| s.parse().ok())
464 .unwrap_or(Address::ZERO)
465}
466
467#[cfg(feature = "erc8004")]
469pub fn validation_registry() -> Address {
470 std::env::var("ERC8004_VALIDATION_REGISTRY")
471 .ok()
472 .and_then(|s| s.parse().ok())
473 .unwrap_or(Address::ZERO)
474}
475
476#[cfg(feature = "erc8004")]
478pub fn auto_mint_enabled() -> bool {
479 std::env::var("ERC8004_AUTO_MINT")
480 .map(|v| v == "true" || v == "1")
481 .unwrap_or(false)
482}
483
484#[cfg(feature = "erc8004")]
486pub fn reputation_enabled() -> bool {
487 std::env::var("ERC8004_REPUTATION_ENABLED")
488 .map(|v| v == "true" || v == "1")
489 .unwrap_or(false)
490}
491
492#[cfg(feature = "erc8004")]
494pub fn recovery_address() -> Option<Address> {
495 std::env::var("ERC8004_RECOVERY_ADDRESS")
496 .ok()
497 .and_then(|s| s.parse().ok())
498}
499
500#[cfg(feature = "erc8004")]
502pub fn load_persisted_registries(path: &str) -> bool {
503 let Ok(data) = std::fs::read_to_string(path) else {
504 return false;
505 };
506 let Ok(json) = serde_json::from_str::<serde_json::Value>(&data) else {
507 return false;
508 };
509
510 let mut loaded = false;
511 for (key, env_var) in [
512 ("identity", "ERC8004_IDENTITY_REGISTRY"),
513 ("reputation", "ERC8004_REPUTATION_REGISTRY"),
514 ("validation", "ERC8004_VALIDATION_REGISTRY"),
515 ] {
516 if let Some(addr) = json.get(key).and_then(|v| v.as_str()) {
517 if std::env::var(env_var).is_err() || std::env::var(env_var).ok().as_deref() == Some("")
518 {
519 std::env::set_var(env_var, addr);
520 loaded = true;
521 }
522 }
523 }
524 loaded
525}
526
527#[cfg(feature = "erc8004")]
529pub fn save_deployed_registries(
530 path: &str,
531 registries: &deploy::DeployedRegistries,
532) -> Result<(), std::io::Error> {
533 let json = serde_json::json!({
534 "identity": format!("{:#x}", registries.identity),
535 "reputation": format!("{:#x}", registries.reputation),
536 "validation": format!("{:#x}", registries.validation),
537 });
538 if let Some(parent) = std::path::Path::new(path).parent() {
539 std::fs::create_dir_all(parent)?;
540 }
541 std::fs::write(path, serde_json::to_string_pretty(&json).unwrap())?;
542 Ok(())
543}
544
545#[cfg(test)]
546mod tests {
547 use super::*;
548 use std::io::Write;
549
550 #[test]
551 fn test_bootstrap_creates_new_identity() {
552 let dir = tempfile::tempdir().unwrap();
553 let path = dir.path().join("identity.json");
554 let path_str = path.to_str().unwrap();
555
556 let identity = bootstrap(path_str).unwrap();
557 assert_ne!(identity.address, Address::ZERO);
558 assert!(!identity.instance_id.is_empty());
559 assert!(path.exists());
560 }
561
562 #[test]
563 fn test_bootstrap_loads_existing_identity() {
564 let dir = tempfile::tempdir().unwrap();
565 let path = dir.path().join("identity.json");
566
567 let signer = PrivateKeySigner::random();
569 let private_key = format!("0x{}", alloy::hex::encode(signer.to_bytes()));
570 let persisted = serde_json::json!({
571 "private_key": private_key,
572 "address": format!("{:#x}", signer.address()),
573 "instance_id": "test-instance",
574 "parent_url": null,
575 "parent_address": null,
576 "created_at": "2025-01-01T00:00:00Z",
577 });
578 let mut file = std::fs::File::create(&path).unwrap();
579 file.write_all(serde_json::to_string_pretty(&persisted).unwrap().as_bytes())
580 .unwrap();
581
582 let path_str = path.to_str().unwrap();
583 let identity = bootstrap(path_str).unwrap();
584 assert_eq!(identity.address, signer.address());
585 assert_eq!(identity.instance_id, "test-instance");
586 }
587
588 #[test]
589 fn test_persisted_roundtrip() {
590 let signer = PrivateKeySigner::random();
591 let identity = InstanceIdentity {
592 private_key: format!("0x{}", alloy::hex::encode(signer.to_bytes())),
593 address: signer.address(),
594 instance_id: "test-123".to_string(),
595 parent_url: Some("https://parent.example.com".to_string()),
596 parent_address: None,
597 created_at: Utc::now(),
598 agent_token_id: None,
599 facilitator_private_key: None,
600 };
601
602 let persisted = PersistedIdentity::from(&identity);
603 let json = serde_json::to_string(&persisted).unwrap();
604 let loaded: PersistedIdentity = serde_json::from_str(&json).unwrap();
605 let restored = InstanceIdentity::try_from(loaded).unwrap();
606
607 assert_eq!(restored.address, identity.address);
608 assert_eq!(restored.instance_id, identity.instance_id);
609 assert_eq!(restored.parent_url, identity.parent_url);
610 }
611}