use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::error::{A2AError, A2AResult};
use crate::reputation::TrustTier;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum AgentSkill {
Buy,
Sell,
Quote,
Negotiate,
Fulfill,
Analytics,
}
impl std::fmt::Display for AgentSkill {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Buy => write!(f, "buy"),
Self::Sell => write!(f, "sell"),
Self::Quote => write!(f, "quote"),
Self::Negotiate => write!(f, "negotiate"),
Self::Fulfill => write!(f, "fulfill"),
Self::Analytics => write!(f, "analytics"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentCard {
pub id: Uuid,
pub name: String,
pub wallet_address: String,
pub public_key: Option<String>,
pub supported_networks: Vec<String>,
pub supported_assets: Vec<String>,
pub skills: Vec<AgentSkill>,
pub endpoint_url: Option<String>,
pub description: String,
pub trust_tier: TrustTier,
pub active: bool,
pub suspended_at: Option<DateTime<Utc>>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl AgentCard {
pub fn new(
name: impl Into<String>,
wallet_address: impl Into<String>,
description: impl Into<String>,
) -> Self {
let now = Utc::now();
Self {
id: Uuid::new_v4(),
name: name.into(),
wallet_address: wallet_address.into(),
public_key: None,
supported_networks: vec!["set_chain".into()],
supported_assets: vec!["USDC".into()],
skills: vec![AgentSkill::Buy, AgentSkill::Sell, AgentSkill::Quote],
endpoint_url: None,
description: description.into(),
trust_tier: TrustTier::Sandbox,
active: true,
suspended_at: None,
created_at: now,
updated_at: now,
}
}
#[allow(clippy::missing_const_for_fn)] #[must_use]
pub fn with_networks(mut self, networks: Vec<String>) -> Self {
self.supported_networks = networks;
self
}
#[allow(clippy::missing_const_for_fn)] #[must_use]
pub fn with_assets(mut self, assets: Vec<String>) -> Self {
self.supported_assets = assets;
self
}
#[allow(clippy::missing_const_for_fn)] #[must_use]
pub fn with_skills(mut self, skills: Vec<AgentSkill>) -> Self {
self.skills = skills;
self
}
#[must_use]
pub const fn with_trust_tier(mut self, tier: TrustTier) -> Self {
self.trust_tier = tier;
self
}
pub fn with_endpoint(mut self, url: impl Into<String>) -> Self {
self.endpoint_url = Some(url.into());
self
}
#[must_use]
pub const fn is_active(&self) -> bool {
self.active && self.suspended_at.is_none()
}
pub fn suspend(&mut self) {
self.active = false;
self.suspended_at = Some(Utc::now());
self.updated_at = Utc::now();
}
pub fn reactivate(&mut self) {
self.active = true;
self.suspended_at = None;
self.updated_at = Utc::now();
}
#[must_use]
pub fn supports_network(&self, network: &str) -> bool {
self.supported_networks.iter().any(|n| n == network)
}
#[must_use]
pub fn supports_asset(&self, asset: &str) -> bool {
self.supported_assets.iter().any(|a| a == asset)
}
#[must_use]
pub fn has_skill(&self, skill: AgentSkill) -> bool {
self.skills.contains(&skill)
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct DiscoveryFilter {
pub network: Option<String>,
pub asset: Option<String>,
pub skill: Option<AgentSkill>,
pub min_trust_tier: Option<TrustTier>,
pub limit: Option<u32>,
}
impl DiscoveryFilter {
#[must_use]
pub fn matches(&self, card: &AgentCard) -> bool {
if !card.is_active() {
return false;
}
if let Some(ref network) = self.network {
if !card.supports_network(network) {
return false;
}
}
if let Some(ref asset) = self.asset {
if !card.supports_asset(asset) {
return false;
}
}
if let Some(skill) = self.skill {
if !card.has_skill(skill) {
return false;
}
}
if let Some(min_tier) = self.min_trust_tier {
if !card.trust_tier.is_at_least(min_tier) {
return false;
}
}
true
}
}
#[must_use]
pub fn filter_agents(cards: &[AgentCard], filter: &DiscoveryFilter) -> Vec<AgentCard> {
let mut results: Vec<AgentCard> = cards.iter().filter(|c| filter.matches(c)).cloned().collect();
results.sort_by(|a, b| b.trust_tier.cmp(&a.trust_tier).then_with(|| a.name.cmp(&b.name)));
if let Some(limit) = filter.limit {
results.truncate(limit as usize);
}
results
}
pub fn validate_agent_card(card: &AgentCard) -> A2AResult<()> {
if card.name.is_empty() {
return Err(A2AError::agent_card("name is required"));
}
if card.wallet_address.is_empty() {
return Err(A2AError::agent_card("wallet_address is required"));
}
if card.description.is_empty() {
return Err(A2AError::agent_card("description is required"));
}
if card.supported_networks.is_empty() {
return Err(A2AError::agent_card("at least one supported network is required"));
}
if card.supported_assets.is_empty() {
return Err(A2AError::agent_card("at least one supported asset is required"));
}
if card.skills.is_empty() {
return Err(A2AError::agent_card("at least one skill is required"));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_card() -> AgentCard {
AgentCard::new("TestAgent", "0xABCD1234", "A test agent")
}
#[test]
fn card_creation() {
let card = sample_card();
assert_eq!(card.name, "TestAgent");
assert_eq!(card.wallet_address, "0xABCD1234");
assert!(card.is_active());
assert_eq!(card.trust_tier, TrustTier::Sandbox);
}
#[test]
fn card_with_networks() {
let card = sample_card().with_networks(vec!["set_chain".into(), "ethereum".into()]);
assert!(card.supports_network("set_chain"));
assert!(card.supports_network("ethereum"));
assert!(!card.supports_network("solana"));
}
#[test]
fn card_with_assets() {
let card = sample_card().with_assets(vec!["USDC".into(), "ssUSD".into()]);
assert!(card.supports_asset("USDC"));
assert!(card.supports_asset("ssUSD"));
assert!(!card.supports_asset("ETH"));
}
#[test]
fn card_with_skills() {
let card = sample_card().with_skills(vec![AgentSkill::Sell, AgentSkill::Quote]);
assert!(card.has_skill(AgentSkill::Sell));
assert!(card.has_skill(AgentSkill::Quote));
assert!(!card.has_skill(AgentSkill::Buy));
}
#[test]
fn card_suspension() {
let mut card = sample_card();
assert!(card.is_active());
card.suspend();
assert!(!card.is_active());
assert!(card.suspended_at.is_some());
card.reactivate();
assert!(card.is_active());
assert!(card.suspended_at.is_none());
}
#[test]
fn validate_valid_card() {
assert!(validate_agent_card(&sample_card()).is_ok());
}
#[test]
fn validate_empty_name() {
let mut card = sample_card();
card.name = String::new();
let err = validate_agent_card(&card).unwrap_err();
assert!(matches!(err, A2AError::AgentCardError(_)));
}
#[test]
fn validate_empty_wallet() {
let mut card = sample_card();
card.wallet_address = String::new();
let err = validate_agent_card(&card).unwrap_err();
assert!(matches!(err, A2AError::AgentCardError(_)));
}
#[test]
fn validate_empty_description() {
let mut card = sample_card();
card.description = String::new();
let err = validate_agent_card(&card).unwrap_err();
assert!(matches!(err, A2AError::AgentCardError(_)));
}
#[test]
fn validate_empty_networks() {
let mut card = sample_card();
card.supported_networks.clear();
let err = validate_agent_card(&card).unwrap_err();
assert!(matches!(err, A2AError::AgentCardError(_)));
}
#[test]
fn validate_empty_assets() {
let mut card = sample_card();
card.supported_assets.clear();
let err = validate_agent_card(&card).unwrap_err();
assert!(matches!(err, A2AError::AgentCardError(_)));
}
#[test]
fn validate_empty_skills() {
let mut card = sample_card();
card.skills.clear();
let err = validate_agent_card(&card).unwrap_err();
assert!(matches!(err, A2AError::AgentCardError(_)));
}
#[test]
fn filter_matches_active_card() {
let card = sample_card();
let filter = DiscoveryFilter::default();
assert!(filter.matches(&card));
}
#[test]
fn filter_rejects_inactive_card() {
let mut card = sample_card();
card.suspend();
let filter = DiscoveryFilter::default();
assert!(!filter.matches(&card));
}
#[test]
fn filter_by_network() {
let card = sample_card();
let filter = DiscoveryFilter { network: Some("set_chain".into()), ..Default::default() };
assert!(filter.matches(&card));
let filter2 = DiscoveryFilter { network: Some("solana".into()), ..Default::default() };
assert!(!filter2.matches(&card));
}
#[test]
fn filter_by_asset() {
let card = sample_card();
let filter = DiscoveryFilter { asset: Some("USDC".into()), ..Default::default() };
assert!(filter.matches(&card));
let filter2 = DiscoveryFilter { asset: Some("ETH".into()), ..Default::default() };
assert!(!filter2.matches(&card));
}
#[test]
fn filter_by_skill() {
let card = sample_card();
let filter = DiscoveryFilter { skill: Some(AgentSkill::Buy), ..Default::default() };
assert!(filter.matches(&card));
let filter2 = DiscoveryFilter { skill: Some(AgentSkill::Analytics), ..Default::default() };
assert!(!filter2.matches(&card));
}
#[test]
fn filter_by_trust_tier() {
let card = sample_card().with_trust_tier(TrustTier::Verified);
let filter =
DiscoveryFilter { min_trust_tier: Some(TrustTier::Standard), ..Default::default() };
assert!(filter.matches(&card));
let filter2 =
DiscoveryFilter { min_trust_tier: Some(TrustTier::Enterprise), ..Default::default() };
assert!(!filter2.matches(&card));
}
#[test]
fn filter_agents_sorted_by_tier_then_name() {
let cards = vec![
AgentCard::new("Charlie", "0x3", "desc").with_trust_tier(TrustTier::Standard),
AgentCard::new("Alice", "0x1", "desc").with_trust_tier(TrustTier::Verified),
AgentCard::new("Bob", "0x2", "desc").with_trust_tier(TrustTier::Verified),
];
let results = filter_agents(&cards, &DiscoveryFilter::default());
assert_eq!(results.len(), 3);
assert_eq!(results[0].name, "Alice");
assert_eq!(results[1].name, "Bob");
assert_eq!(results[2].name, "Charlie");
}
#[test]
fn filter_agents_respects_limit() {
let cards: Vec<AgentCard> = (0..10)
.map(|i| AgentCard::new(format!("Agent{i}"), format!("0x{i}"), "desc"))
.collect();
let filter = DiscoveryFilter { limit: Some(3), ..Default::default() };
let results = filter_agents(&cards, &filter);
assert_eq!(results.len(), 3);
}
#[test]
fn filter_agents_excludes_suspended() {
let mut card = sample_card();
card.suspend();
let results = filter_agents(&[card], &DiscoveryFilter::default());
assert!(results.is_empty());
}
#[test]
fn skill_display() {
assert_eq!(AgentSkill::Buy.to_string(), "buy");
assert_eq!(AgentSkill::Sell.to_string(), "sell");
assert_eq!(AgentSkill::Quote.to_string(), "quote");
assert_eq!(AgentSkill::Negotiate.to_string(), "negotiate");
assert_eq!(AgentSkill::Fulfill.to_string(), "fulfill");
assert_eq!(AgentSkill::Analytics.to_string(), "analytics");
}
#[test]
fn card_serde_roundtrip() {
let card = sample_card();
let json = serde_json::to_string(&card).unwrap();
let parsed: AgentCard = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.name, card.name);
assert_eq!(parsed.wallet_address, card.wallet_address);
}
}