diff --git a/.taskmaster/docs/prd.txt b/.taskmaster/docs/prd.txt index 00293be..cfa074f 100644 --- a/.taskmaster/docs/prd.txt +++ b/.taskmaster/docs/prd.txt @@ -28,11 +28,35 @@ Transform AI agents from passive tools into active economic participants capable ### 1. Registry Systems -#### Service Registry -- **REQ-1.1**: Maintain an open catalog of available services -- **REQ-1.2**: Support service metadata, pricing, and capability definitions -- **REQ-1.3**: Enable community-driven service additions (future) -- **REQ-1.4**: Provide service discovery and filtering mechanisms +#### Service Registry V2 +- **REQ-1.1**: Maintain an open catalog of available services with full lifecycle management +- **REQ-1.2**: Implement hybrid on-chain/off-chain architecture for optimal gas efficiency: + - **On-chain data** (stored in ServiceRegistry contract): + - Service ID (auto-increment from blockchain) + - Owner address (service creator/maintainer) + - Agent address (assigned executor, optional) + - Service URI (IPFS hash for metadata storage) + - Status (draft, published, archived, deleted) + - Version number (for cache invalidation) + - **Off-chain metadata** (stored in IPFS): + - Service name and detailed information + - Service description and technical specifications + - Creation and update timestamps (for gas efficiency) + - Category and tags for discovery + - Technical specifications (endpoint URLs, HTTP methods, parameter/result schemas) + - Business configurations (pricing models, rate limits) + - Operational status from monitoring systems +- **REQ-1.3**: Enable service-agent assignment model: + - Services can be created independently without agent assignment + - Services can be assigned/unassigned to agents dynamically + - Unassigned services remain inactive but preserved + - Support for service transfer between agents + - Published services must have an agent assigned (enforced by schema validation) +- **REQ-1.4**: Provide advanced service discovery and filtering: + - On-chain filtering by owner, agent, name, status + - Off-chain filtering by category, pricing, detailed specifications + - Hybrid queries combining blockchain and IPFS data + - Search unassigned services for marketplace discovery #### Agent Registry - **REQ-1.5**: Allow agents to self-register with metadata and capabilities @@ -126,6 +150,67 @@ Transform AI agents from passive tools into active economic participants capable - Validation: Property-specific validation using Zod schemas - Use case: Targeted updates like status changes, adding attributes, or updating social links +- **REQ-4.1.2**: Provide comprehensive service registry integration APIs + - **ServiceRegistryService.registerService(params)**: Register new service with IPFS metadata storage + - Parameters: RegisterServiceParams with name, agentAddress (optional), metadata object + - Metadata includes: description, category, endpointSchema, method, parametersSchema, resultSchema, tags, pricing + - Returns: Promise with complete service data (on-chain + off-chain combined) + - Features: Automatic IPFS upload, blockchain registration, Zod validation + - Process: Upload metadata to IPFS → Register minimal data on-chain → Return combined ServiceRecord + + - **ServiceRegistryService.getService(serviceId)**: Get complete service record + - Returns: ServiceRecord combining on-chain data with IPFS metadata + - Includes: All service fields flattened for ease of use + - Handles: IPFS metadata fetching and merging with blockchain data + + - **ServiceRegistryService.updateService(serviceId, updates)**: Update service with flexible parameters + - Parameters: serviceId (string), UpdateServiceParams with optional on-chain and metadata fields + - On-chain updates: name, agentAddress, status + - Off-chain updates: metadata object with any combination of description, category, technical specs, pricing + - Returns: Promise with updated combined data + - Features: Selective IPFS updates, version management, ownership validation + + - **ServiceRegistryService.getAllServices(filters)**: Query services with advanced filtering + - Filter parameters: category, status, owner, agent, pricing model + - Returns: Array of ServiceRecord objects matching criteria + - Supports: Pagination, sorting, hybrid on-chain/off-chain filtering + + - **ServiceRegistryService.activateService(serviceId)**: Change service status to published + - Validation: Ensures agent is assigned before activation + - Updates: On-chain status and version increment + - Returns: Promise with updated status + +- **REQ-4.1.3**: Service schema architecture and validation system + - **Three-Schema Architecture**: Clear separation of concerns for optimal performance + - ServiceOnChainSchema: Minimal blockchain data (8 fields) + - ServiceMetadataSchema: Rich IPFS metadata (9+ fields including operational status) + - ServiceRecordSchema: Complete combined view for SDK users + + - **Service Status Lifecycle**: Simplified 4-state model aligned with business needs + - draft: Service being configured, not ready for use + - published: Service available for discovery and execution + - archived: Service deprecated but preserved for history + - deleted: Service removed (soft delete for data integrity) + + - **Comprehensive Validation**: Zod-based validation with enhanced error reporting + - validateServiceRecord(): Complete service validation + - validateServiceOnChain(): On-chain data validation + - validateServiceMetadata(): IPFS metadata validation + - validateRegisterServiceParams(): Registration parameter validation + - validateUpdateServiceParams(): Update parameter validation with partial updates + + - **Type Safety and Developer Experience**: Full TypeScript support + - ServiceRecord: Primary type for SDK consumers + - ServiceOnChain: Blockchain data type + - ServiceMetadata: IPFS metadata type + - RegisterServiceParams: Registration input type + - UpdateServiceParams: Update input type with flexible partial updates + + - **Backward Compatibility**: Legacy aliases and migration support + - Service type alias → ServiceRecord + - Deprecated function aliases with clear migration path + - Gradual migration strategy for existing integrations + - **REQ-4.2**: Support task discovery and proposal submission - **REQ-4.3**: Enable real-time task notifications - **REQ-4.4**: Provide payment and reputation management tools diff --git a/.taskmaster/tasks/tasks.json b/.taskmaster/tasks/tasks.json index ef2c5ce..f2ac5b3 100644 --- a/.taskmaster/tasks/tasks.json +++ b/.taskmaster/tasks/tasks.json @@ -331,11 +331,94 @@ ], "priority": "high", "subtasks": [] + }, + { + "id": 25, + "title": "SDK: Implement Service Management System V2 with comprehensive CRUD operations, ownership model, and agent assignment capabilities", + "description": "Implement a comprehensive Service Management System V2 in the TypeScript SDK with full CRUD operations, ownership model, agent assignment capabilities, hybrid storage (on-chain + IPFS), and advanced discovery/filtering as specified in the PRD.", + "details": "1. ServiceRegistryService Enhancement:\n - Extend existing ServiceRegistryService class with V2 capabilities\n - Implement comprehensive CRUD operations: createService(), updateService(), deleteService(), getService(), listServices()\n - Add ownership model with transferOwnership(), getServiceOwner(), and ownership validation\n - Implement agent assignment/unassignment: assignAgent(), unassignAgent(), getAssignedAgents(), getServicesByAgent()\n - Add service lifecycle management with status tracking (draft, active, paused, archived, deleted)\n\n2. ServiceSchema Integration:\n - Create comprehensive ServiceSchema interface with required fields: id, name, description, category, owner, status, createdAt, updatedAt\n - Add optional fields: tags, metadata, pricing, requirements, capabilities, assignedAgents\n - Implement Zod validation schemas for all service operations\n - Support nested schema validation for complex service configurations\n\n3. Smart Contract Updates:\n - Extend ServiceRegistry.sol with V2 functionality including ownership transfers, agent assignments, and enhanced metadata\n - Implement hybrid storage pattern: core data on-chain (id, owner, status, assignedAgents) and metadata on IPFS\n - Add events for ServiceCreated, ServiceUpdated, ServiceDeleted, AgentAssigned, AgentUnassigned, OwnershipTransferred\n - Implement access control with onlyOwner modifiers and agent assignment permissions\n\n4. Hybrid Storage Implementation:\n - Integrate IPFS client for metadata storage using ipfs-http-client\n - Implement automatic IPFS pinning for service metadata and large data objects\n - Create metadata synchronization between on-chain references and IPFS content\n - Add content addressing and integrity verification for IPFS stored data\n\n5. Advanced Discovery and Filtering:\n - Implement advanced search with filters: category, status, owner, assignedAgent, tags, dateRange\n - Add pagination support with cursor-based navigation for large result sets\n - Implement sorting options: createdAt, updatedAt, name, category, status\n - Create full-text search capabilities for service names and descriptions\n - Add geolocation-based filtering if location metadata is available\n\n6. CLI Service Commands Integration:\n - Create 'ensemble services' command group with subcommands: create, list, get, update, delete, assign, unassign, transfer\n - Implement service-record.yaml configuration file support similar to agent-record.yaml\n - Add interactive service creation wizard with step-by-step guidance\n - Support bulk operations for service management and agent assignments", + "testStrategy": "1. Unit Testing:\n - Test all ServiceRegistryService methods with mocked blockchain interactions and IPFS operations\n - Validate ServiceSchema Zod validation with valid and invalid service data structures\n - Test ownership model operations including transfers and permission checks\n - Verify agent assignment/unassignment logic with various scenarios (single/multiple agents)\n - Test hybrid storage operations with mocked IPFS client responses\n\n2. Integration Testing:\n - Deploy updated smart contracts to testnet and test all V2 functionality end-to-end\n - Test IPFS integration with real IPFS nodes and verify metadata storage/retrieval\n - Validate service lifecycle management across different status transitions\n - Test advanced discovery and filtering with large datasets (1000+ services)\n - Verify CLI commands integration with real SDK operations\n\n3. Performance Testing:\n - Benchmark service creation/update operations with large metadata objects\n - Test pagination performance with datasets of varying sizes (100, 1K, 10K services)\n - Measure IPFS upload/download times for different metadata sizes\n - Test concurrent agent assignment operations and verify data consistency\n\n4. Security Testing:\n - Verify ownership validation prevents unauthorized service modifications\n - Test agent assignment permissions and access control mechanisms\n - Validate IPFS content integrity and prevent metadata tampering\n - Test smart contract upgrade scenarios and data migration safety\n\n5. End-to-End Testing:\n - Create complete service management workflows from CLI to blockchain\n - Test service discovery through various filtering combinations\n - Verify ownership transfers maintain data integrity and agent assignments\n - Test service deletion and cleanup of associated IPFS metadata", + "status": "pending", + "dependencies": [ + 1, + 3, + 21 + ], + "priority": "medium", + "subtasks": [ + { + "id": 1, + "title": "Extend ServiceRegistryService with V2 CRUD Operations and Ownership Model", + "description": "Enhance the existing ServiceRegistryService class with comprehensive CRUD operations, ownership model, and service lifecycle management capabilities.", + "dependencies": [], + "details": "Extend the ServiceRegistryService class to include: createService(), updateService(), deleteService(), getService(), listServices() methods with proper error handling and validation. Implement ownership model with transferOwnership(), getServiceOwner(), and ownership validation methods. Add service lifecycle management with status tracking (draft, active, paused, archived, deleted). Include proper TypeScript interfaces and error handling for all operations. Ensure backward compatibility with existing service registry functionality.", + "status": "done", + "testStrategy": "Unit tests for all CRUD operations with mocked blockchain interactions. Test ownership model operations including transfers and permission checks. Validate service lifecycle state transitions and status management." + }, + { + "id": 2, + "title": "Create ServiceSchema Interface and Zod Validation", + "description": "Design and implement comprehensive ServiceSchema interface with required and optional fields, along with Zod validation schemas for all service operations.", + "dependencies": [ + "25.1" + ], + "details": "Create ServiceSchema interface with required fields: id, name, description, category, owner, status, createdAt, updatedAt. Add optional fields: tags, metadata, pricing, requirements, capabilities, assignedAgents. Implement Zod validation schemas for service creation, updates, and queries. Support nested schema validation for complex service configurations. Create type-safe validation functions that can be used across the SDK. Include proper error messages and validation feedback for invalid schemas.", + "status": "done", + "testStrategy": "Validate ServiceSchema Zod validation with valid and invalid service data structures. Test nested schema validation for complex configurations. Verify type safety and error message clarity." + }, + { + "id": 3, + "title": "Update Smart Contracts with V2 Functionality and Hybrid Storage", + "description": "Extend ServiceRegistry.sol smart contract with V2 functionality including ownership transfers, agent assignments, and hybrid storage pattern implementation.", + "dependencies": [ + "25.2" + ], + "details": "Extend ServiceRegistry.sol with V2 functionality including ownership transfers, agent assignments, and enhanced metadata support. Implement hybrid storage pattern storing core data on-chain (id, owner, status, assignedAgents) and metadata on IPFS. Add events for ServiceCreated, ServiceUpdated, ServiceDeleted, AgentAssigned, AgentUnassigned, OwnershipTransferred. Implement access control with onlyOwner modifiers and agent assignment permissions. Include gas optimization and proper event indexing for efficient querying.", + "status": "done", + "testStrategy": "Smart contract unit tests for all new functions and access controls. Test hybrid storage pattern with IPFS integration. Verify event emissions and gas optimization. Test ownership transfers and agent assignment permissions." + }, + { + "id": 4, + "title": "Implement Agent Assignment and IPFS Integration", + "description": "Add agent assignment capabilities and integrate IPFS client for metadata storage with automatic pinning and content verification.", + "dependencies": [ + "25.3" + ], + "details": "Implement agent assignment/unassignment methods: assignAgent(), unassignAgent(), getAssignedAgents(), getServicesByAgent() with proper validation and blockchain integration. Integrate IPFS client using ipfs-http-client for metadata storage. Implement automatic IPFS pinning for service metadata and large data objects. Create metadata synchronization between on-chain references and IPFS content. Add content addressing and integrity verification for IPFS stored data. Include retry logic and error handling for IPFS operations.", + "status": "done", + "testStrategy": "Test agent assignment operations with ownership validation. Verify IPFS integration with metadata storage and retrieval. Test automatic pinning and content integrity verification. Validate synchronization between on-chain and IPFS data." + }, + { + "id": 5, + "title": "Implement Advanced Discovery, Filtering, and CLI Integration", + "description": "Create advanced search and filtering capabilities with pagination, and integrate CLI service commands with interactive wizards and bulk operations.", + "dependencies": [ + "25.4" + ], + "details": "Implement advanced search with filters: category, status, owner, assignedAgent, tags, dateRange. Add pagination support with cursor-based navigation for large result sets. Implement sorting options: createdAt, updatedAt, name, category, status. Create full-text search capabilities for service names and descriptions. Add geolocation-based filtering if location metadata is available. Create 'ensemble services' command group with subcommands: create, list, get, update, delete, assign, unassign, transfer. Implement service-record.yaml configuration file support. Add interactive service creation wizard and support bulk operations.", + "status": "pending", + "testStrategy": "Test advanced filtering and search functionality with various query combinations. Validate pagination and sorting with large datasets. Test CLI commands with interactive wizards and configuration file parsing. Verify bulk operations and error handling." + } + ] + }, + { + "id": 26, + "title": "SDK: Refactor to Service-Based API Architecture", + "description": "Refactor the TypeScript SDK to eliminate duplicate wrapper methods from the Ensemble class and implement a clean service-based architecture with organized modules (ensemble.services.*, ensemble.agents.*, ensemble.tasks.*).", + "details": "1. Architecture Analysis and Planning:\n - Audit existing Ensemble class to identify duplicate wrapper methods and redundant functionality\n - Design new service-based architecture with clear separation of concerns\n - Create module structure: ensemble.services.* (core services), ensemble.agents.* (agent-specific operations), ensemble.tasks.* (task management)\n - Define interfaces and contracts for each service module to ensure consistency\n\n2. Service Module Implementation:\n - Create BaseService abstract class with common functionality (error handling, validation, logging)\n - Implement ensemble.services.registry for core blockchain registry operations\n - Implement ensemble.services.payment for payment and transaction management\n - Implement ensemble.services.storage for IPFS and data persistence operations\n - Implement ensemble.services.websocket for real-time communication handling\n\n3. Agent Module Refactoring:\n - Create ensemble.agents.manager for agent lifecycle management (register, update, delete)\n - Implement ensemble.agents.discovery for agent search and filtering capabilities\n - Create ensemble.agents.validator for agent data validation and schema enforcement\n - Implement ensemble.agents.metadata for agent profile and attribute management\n\n4. Task Module Implementation:\n - Create ensemble.tasks.manager for task creation, assignment, and lifecycle management\n - Implement ensemble.tasks.discovery for task search and matching algorithms\n - Create ensemble.tasks.execution for task execution monitoring and status tracking\n - Implement ensemble.tasks.proposals for proposal submission and evaluation\n\n5. Ensemble Class Refactoring:\n - Remove duplicate wrapper methods and consolidate functionality into appropriate services\n - Implement dependency injection pattern for service composition\n - Create factory methods for service instantiation with proper configuration\n - Maintain backward compatibility through deprecation warnings and adapter patterns\n - Update all method signatures to use service-based approach while preserving existing API contracts\n\n6. Configuration and Initialization:\n - Implement centralized configuration management for all services\n - Create service registry for dependency resolution and lifecycle management\n - Add environment-specific configuration support (development, staging, production)\n - Implement proper error handling and logging across all service modules", + "testStrategy": "1. Unit Testing:\n - Test each service module independently with comprehensive mocking of dependencies\n - Verify BaseService abstract class functionality and inheritance patterns\n - Test service factory methods and dependency injection mechanisms\n - Validate configuration management and environment-specific settings\n - Test backward compatibility adapters and deprecation warning systems\n\n2. Integration Testing:\n - Test service composition and inter-service communication patterns\n - Verify proper error propagation between service layers\n - Test service lifecycle management and cleanup procedures\n - Validate configuration loading and service initialization sequences\n - Test real blockchain interactions through refactored service architecture\n\n3. Migration Testing:\n - Create comprehensive test suite comparing old Ensemble class behavior with new service-based implementation\n - Test all existing SDK consumers (CLI, agents) to ensure no breaking changes\n - Verify performance characteristics remain consistent or improve after refactoring\n - Test memory usage and resource cleanup in new architecture\n - Validate WebSocket connections and real-time features work correctly through new service structure\n\n4. End-to-End Testing:\n - Test complete agent registration workflow through new service architecture\n - Verify task discovery and execution flows work correctly\n - Test payment processing and blockchain transaction handling\n - Validate IPFS storage operations and metadata management\n - Test error scenarios and recovery mechanisms across all service modules", + "status": "pending", + "dependencies": [ + 3, + 25 + ], + "priority": "medium", + "subtasks": [] } ], "metadata": { "created": "2025-07-20T10:42:18.955Z", - "updated": "2025-08-22T06:26:14.340Z", + "updated": "2025-08-28T20:57:44.965Z", "description": "Tasks for master context" } } diff --git a/CLAUDE.md b/CLAUDE.md index 11c5cae..2e36e60 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -171,4 +171,11 @@ task-master add-task # DON'T DO THIS # ✅ CORRECT - Always work from project root for tasks cd /Users/leon/workspace/ensemble/ensemble-framework task-master add-task --prompt="CLI: Add new command for..." -``` \ No newline at end of file +``` + +## Project Documentation + +### PRD File Location +The Project Requirements Document (PRD) is located at: `.taskmaster/docs/prd.txt` + +When updating project specifications, features, or requirements, update the PRD file to maintain alignment between implementation and documentation. \ No newline at end of file diff --git a/packages/contracts/contracts/AgentsRegistryUpgradeable.sol b/packages/contracts/contracts/AgentsRegistryUpgradeable.sol index eda29da..e69d2d4 100644 --- a/packages/contracts/contracts/AgentsRegistryUpgradeable.sol +++ b/packages/contracts/contracts/AgentsRegistryUpgradeable.sol @@ -5,7 +5,6 @@ import "./ServiceRegistryUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; -import "./interfaces/IProposalStruct.sol"; import "./interfaces/IAgentRegistryV1.sol"; // Interface for V1 ServiceRegistry compatibility @@ -23,10 +22,10 @@ interface IServiceRegistryV1 { /** * @title AgentsRegistryUpgradeable * @author leonprou - * @notice A smart contract that stores information about the agents, and the services proposals provided by the agents. - * @dev Upgradeable version using UUPS proxy pattern + * @notice A smart contract that manages agent registration and reputation. + * @dev Upgradeable version using UUPS proxy pattern for Agent Management V2 */ -contract AgentsRegistryUpgradeable is Initializable, OwnableUpgradeable, UUPSUpgradeable, IProposalStruct { +contract AgentsRegistryUpgradeable is Initializable, OwnableUpgradeable, UUPSUpgradeable { struct AgentData { string name; string agentUri; @@ -38,11 +37,10 @@ contract AgentsRegistryUpgradeable is Initializable, OwnableUpgradeable, UUPSUpg IAgentRegistryV1 public agentRegistryV1; ServiceRegistryUpgradeable public serviceRegistry; - address public taskRegistry; mapping(address => AgentData) public agents; - mapping(uint256 => ServiceProposal) public proposals; - uint256 public nextProposalId; + // Mapping to track if a user has already rated an agent: agent => rater => hasRated + mapping(address => mapping(address => bool)) public hasRated; modifier onlyAgentOwner(address agent) { require( @@ -71,7 +69,6 @@ contract AgentsRegistryUpgradeable is Initializable, OwnableUpgradeable, UUPSUpg agentRegistryV1 = _agentRegistryV1; serviceRegistry = _serviceRegistry; - nextProposalId = 1; } event AgentRegistered( @@ -81,41 +78,16 @@ contract AgentsRegistryUpgradeable is Initializable, OwnableUpgradeable, UUPSUpg string agentUri ); event ReputationUpdated(address indexed agent, uint256 newReputation); - - event ProposalAdded( - address indexed agent, - uint256 proposalId, - string name, - uint256 price, - address tokenAddress - ); - event ProposalRemoved(address indexed agent, uint256 proposalId); - - event ProposalUpdated( - address indexed agent, - uint256 proposalId, - uint256 price, - address tokenAddress - ); event AgentDataUpdated( address indexed agent, string name, string agentUri ); - event AgentRemoved( address indexed agent, address indexed owner ); - /** - * @dev Sets the address of the TaskRegistry contract. - * @param _taskRegistry The address of the TaskRegistry contract. - */ - function setTaskRegistry(address _taskRegistry) external onlyOwner { - require(_taskRegistry != address(0), "Invalid address"); - taskRegistry = _taskRegistry; - } /** * @dev Sets the address of the ServiceRegistry contract. @@ -150,99 +122,7 @@ contract AgentsRegistryUpgradeable is Initializable, OwnableUpgradeable, UUPSUpg } /** - * @dev Registers a new agent with the given details and the proposal. - * @param name The name of the agent. - * @param agentUri The URI pointing to the agent's metadata. - * @param agent The address of the agent. - * @param serviceName The name of the service. - * @param servicePrice The price of the service. - * - * Requirements: - * - * - The agent must not already be registered. - * - The caller will be set as the owner of the agent. - * - * Emits an {AgentRegistered} event. - */ - function registerAgentWithService( - address agent, - string memory name, - string memory agentUri, - string memory serviceName, - uint256 servicePrice, - address tokenAddress - ) external { - require(agents[agent].agent == address(0), "Agent already registered"); - require( - serviceRegistry.isServiceRegistered(serviceName), - "Service not registered" - ); - - _createAgent(agent, name, agentUri, msg.sender, 0); - - _createProposal(agent, serviceName, servicePrice, tokenAddress); - } - - /** - * @dev Adds a new proposal for an agent. - * @param agent The address of the agent. - * @param serviceName The name of the service. - * @param servicePrice The price of the service. - * - * Requirements: - * - * - The caller must be the owner of the agent. - * - The agent must be registered. - * - The service must be registered. - * - * Emits a {ProposalAdded} event. - */ - function addProposal( - address agent, - string memory serviceName, - uint256 servicePrice, - address tokenAddress - ) public onlyAgentOwner(agent) { - require( - serviceRegistry.isServiceRegistered(serviceName), - "Service not registered" - ); - - _createProposal(agent, serviceName, servicePrice, tokenAddress); - } - - /** - * @dev Removes a proposal for an agent. - * @param agent The address of the agent. - * @param proposalId The ID of the proposal to remove. - * @return true if the proposal was removed successfully, false otherwise. - * - * Requirements: - * - * - The caller must be the owner of the agent. - * - The agent must be registered. - * - The proposal must exist. - * - * Emits a {ProposalRemoved} event. - */ - function removeProposal( - address agent, - uint256 proposalId - ) external onlyAgentOwner(agent) returns (bool) { - require( - proposals[proposalId].issuer == agent, - "ServiceProposal not found" - ); - - delete proposals[proposalId]; - - emit ProposalRemoved(agent, proposalId); - - return true; - } - - /** - * @dev Migrates an agent. + * @dev Migrates an agent from V1 registry. * @param agent The address of the agent. */ function migrateAgent(address agent) external { @@ -256,19 +136,35 @@ contract AgentsRegistryUpgradeable is Initializable, OwnableUpgradeable, UUPSUpg ); _createAgent(agent, v1AgentData.name, v1AgentData.agentUri, v1AgentData.owner, v1AgentData.reputation); - - _migrateAgentProposals(agent); } + /** + * @dev Adds a rating to an agent (one rating per user). + * @param agent The address of the agent. + * @param _rating The rating value (0-100). + * @return The new reputation score. + */ function addRating( address agent, uint256 _rating ) public returns (uint256) { - require(msg.sender == taskRegistry, "Not the TaskRegistry contract"); + require( + agents[agent].agent != address(0), + "Agent not registered" + ); + require( + !hasRated[agent][msg.sender], + "User has already rated this agent" + ); require( _rating >= 0 && _rating <= 100, "Rating must be between 0 and 100" ); + + // Mark that this user has rated this agent + hasRated[agent][msg.sender] = true; + + // Update agent reputation agents[agent].totalRatings += 1; agents[agent].reputation = (agents[agent].reputation * @@ -280,10 +176,25 @@ contract AgentsRegistryUpgradeable is Initializable, OwnableUpgradeable, UUPSUpg return agents[agent].reputation; } + /** + * @dev Gets the reputation of an agent. + * @param agent The address of the agent. + * @return The reputation score. + */ function getReputation(address agent) external view returns (uint256) { return agents[agent].reputation; } + /** + * @dev Checks if a user has already rated an agent. + * @param agent The address of the agent. + * @param rater The address of the rater. + * @return True if the rater has already rated the agent. + */ + function hasUserRated(address agent, address rater) external view returns (bool) { + return hasRated[agent][rater]; + } + /** * @dev Returns the data of an agent. * @param _agent The address of the agent. @@ -296,12 +207,6 @@ contract AgentsRegistryUpgradeable is Initializable, OwnableUpgradeable, UUPSUpg return data; } - function getProposal( - uint256 proposalId - ) external view returns (ServiceProposal memory) { - return proposals[proposalId]; - } - /** * @dev Sets the data of an existing agent. * @param agent The address of the agent to update. @@ -329,7 +234,7 @@ contract AgentsRegistryUpgradeable is Initializable, OwnableUpgradeable, UUPSUpg } /** - * @dev Removes an existing agent and removes all associated proposals. + * @dev Removes an existing agent. * @param agent The address of the agent to remove. * * Requirements: @@ -337,22 +242,22 @@ contract AgentsRegistryUpgradeable is Initializable, OwnableUpgradeable, UUPSUpg * - The caller must be the owner of the agent. * - The agent must be registered. * - * Emits an {AgentRemoved} event and {ProposalRemoved} events for each removed proposal. + * Emits an {AgentRemoved} event. */ function removeAgent(address agent) external onlyAgentOwner(agent) { require(agents[agent].agent != address(0), "Agent not registered"); address agentOwner = agents[agent].owner; - // Remove all active proposals for this agent - _removeAllAgentProposals(agent); - // Clear agent data delete agents[agent]; emit AgentRemoved(agent, agentOwner); } + /** + * @dev Internal function to create an agent. + */ function _createAgent( address agent, string memory name, @@ -370,81 +275,6 @@ contract AgentsRegistryUpgradeable is Initializable, OwnableUpgradeable, UUPSUpg emit AgentRegistered(agent, owner, name, agentUri); } - function _createProposal( - address agent, - string memory serviceName, - uint256 servicePrice, - address tokenAddress - ) private { - ServiceProposal memory newProposal = ServiceProposal( - agent, - serviceName, - servicePrice, - tokenAddress, - nextProposalId, - true - ); - - proposals[nextProposalId] = newProposal; - - emit ProposalAdded(agent, nextProposalId, serviceName, servicePrice, tokenAddress); - - nextProposalId++; - } - - function _ensureServiceRegistered(string memory serviceName) private { - if (!serviceRegistry.isServiceRegistered(serviceName)) { - address serviceRegistryV1Addr = IAgentRegistryV1(agentRegistryV1) - .serviceRegistry(); - - IServiceRegistryV1 serviceRegistryV1 = IServiceRegistryV1( - serviceRegistryV1Addr - ); - - IServiceRegistryV1.Service memory service = serviceRegistryV1 - .getService(serviceName); - - serviceRegistry.registerService( - service.name, - service.category, - service.description - ); - } - } - - function _migrateAgentProposals(address agent) private { - uint256 numProposalsRegistered = IAgentRegistryV1(agentRegistryV1) - .nextProposalId(); - - for (uint256 i = 0; i < numProposalsRegistered; i++) { - IAgentRegistryV1.Proposal memory proposal = IAgentRegistryV1( - agentRegistryV1 - ).getProposal(i); - - if (proposal.issuer != agent || !proposal.isActive) { - continue; - } - - _ensureServiceRegistered(proposal.serviceName); - - _createProposal(agent, proposal.serviceName, proposal.price, address(0)); - } - } - - /** - * @dev Internal function to remove all proposals associated with an agent. - * @param agent The address of the agent whose proposals should be removed. - */ - function _removeAllAgentProposals(address agent) private { - // Iterate through all proposals to find and remove agent's proposals - for (uint256 i = 1; i < nextProposalId; i++) { - if (proposals[i].issuer == agent && proposals[i].isActive) { - delete proposals[i]; - emit ProposalRemoved(agent, i); - } - } - } - /** * @dev Function that should revert when `msg.sender` is not authorized to upgrade the contract. * @param newImplementation Address of the new implementation @@ -455,4 +285,4 @@ contract AgentsRegistryUpgradeable is Initializable, OwnableUpgradeable, UUPSUpg * @dev Storage gap for future upgrades */ uint256[50] private __gap; -} \ No newline at end of file +} \ No newline at end of file diff --git a/packages/contracts/contracts/ServiceRegistryUpgradeable.sol b/packages/contracts/contracts/ServiceRegistryUpgradeable.sol index f78b718..5057806 100644 --- a/packages/contracts/contracts/ServiceRegistryUpgradeable.sol +++ b/packages/contracts/contracts/ServiceRegistryUpgradeable.sol @@ -8,21 +8,42 @@ import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; /** * @title ServiceRegistryUpgradeable * @author leonprou - * @notice A smart contract that stores information about the services provided by agents. - * @dev Upgradeable version using UUPS proxy pattern + * @notice A smart contract that manages service registration with hybrid on-chain/off-chain architecture. + * @dev Upgradeable version using UUPS proxy pattern for Service Management V2 */ contract ServiceRegistryUpgradeable is Initializable, OwnableUpgradeable, UUPSUpgradeable { + + enum ServiceStatus { + DRAFT, // 0 - Newly created, not published + PUBLISHED, // 1 - Active and discoverable + ARCHIVED, // 2 - Inactive but preserved + DELETED // 3 - Soft deleted (hidden) + } + struct Service { - string name; - string category; - string description; + uint256 id; // Auto-incremented service ID + address owner; // Service owner address + address agentAddress; // Assigned agent (zero address if none) + string serviceUri; // IPFS URI for metadata (contains name & metadata) + ServiceStatus status; // Service lifecycle status + uint256 version; // Version for cache invalidation } - mapping(string => Service) public services; - uint256 public serviceCount; + // Storage mappings + mapping(uint256 => Service) public services; // serviceId => Service + mapping(address => uint256[]) public servicesByOwner; // owner => serviceId[] + mapping(address => uint256[]) public servicesByAgent; // agent => serviceId[] - event ServiceRegistered(string name, string category, string description); - event ServiceUpdated(string name, string category, string description); + uint256 public nextServiceId; + uint256 public totalServices; + + // Events + event ServiceRegistered(uint256 indexed serviceId, address indexed owner, string serviceUri); + event ServiceUpdated(uint256 indexed serviceId, string serviceUri, uint256 version); + event ServiceStatusChanged(uint256 indexed serviceId, ServiceStatus indexed oldStatus, ServiceStatus indexed newStatus); + event ServiceOwnershipTransferred(uint256 indexed serviceId, address indexed oldOwner, address indexed newOwner); + event ServiceAgentAssigned(uint256 indexed serviceId, address indexed agent); + event ServiceAgentUnassigned(uint256 indexed serviceId, address indexed agent); /// @custom:oz-upgrades-unsafe-allow constructor constructor() { @@ -35,56 +56,324 @@ contract ServiceRegistryUpgradeable is Initializable, OwnableUpgradeable, UUPSUp function initialize() public initializer { __Ownable_init(msg.sender); __UUPSUpgradeable_init(); - serviceCount = 0; + nextServiceId = 0; + totalServices = 0; + } + + // ============================================================================ + // Modifiers + // ============================================================================ + + modifier onlyServiceOwner(uint256 serviceId) { + require(services[serviceId].owner == msg.sender, "Not service owner"); + _; + } + + modifier serviceExists(uint256 serviceId) { + require(serviceId > 0 && serviceId <= nextServiceId, "Service does not exist"); + _; } + // ============================================================================ + // Core CRUD Operations + // ============================================================================ + /** - * @dev Registers a new service with the given name and price. - * @param name The name of the service. - * @return The ID of the registered service. + * @dev Registers a new service with hybrid on-chain/off-chain architecture. + * @param serviceUri IPFS URI containing service metadata (including name). + * @param agentAddress Optional agent address to assign (zero address if none). + * @return serviceId The auto-incremented service ID. */ - function registerService(string memory name, string memory category, string memory description) external returns (Service memory) { - require(!this.isServiceRegistered(name), "Service already registered"); - - Service memory service = Service({ - name: name, - category: category, - description: description + function registerService( + string memory serviceUri, + address agentAddress + ) external returns (uint256 serviceId) { + require(bytes(serviceUri).length > 0, "Service URI required"); + + serviceId = ++nextServiceId; + + services[serviceId] = Service({ + id: serviceId, + owner: msg.sender, + agentAddress: agentAddress, + serviceUri: serviceUri, + status: ServiceStatus.DRAFT, + version: 1 }); + + servicesByOwner[msg.sender].push(serviceId); + + if (agentAddress != address(0)) { + servicesByAgent[agentAddress].push(serviceId); + } + + totalServices++; + + emit ServiceRegistered(serviceId, msg.sender, serviceUri); + if (agentAddress != address(0)) { + emit ServiceAgentAssigned(serviceId, agentAddress); + } + } - services[name] = service; + /** + * @dev Retrieves a service by its ID. + * @param serviceId The ID of the service to retrieve. + * @return The service details. + */ + function getService(uint256 serviceId) external view serviceExists(serviceId) returns (Service memory) { + return services[serviceId]; + } - emit ServiceRegistered(name, category, description); + // V2: getServiceByName removed - names are stored off-chain in IPFS metadata - serviceCount++; - return service; + /** + * @dev Updates a service's metadata URI. + * @param serviceId The ID of the service to update. + * @param serviceUri The new IPFS URI for service metadata. + */ + function updateService(uint256 serviceId, string memory serviceUri) + external + serviceExists(serviceId) + onlyServiceOwner(serviceId) + { + require(bytes(serviceUri).length > 0, "Service URI required"); + + Service storage service = services[serviceId]; + require(service.status != ServiceStatus.DELETED, "Cannot update deleted service"); + + service.serviceUri = serviceUri; + service.version++; + + emit ServiceUpdated(serviceId, serviceUri, service.version); } + + /** + * @dev Sets the status of a service with validation. + * @param serviceId The ID of the service. + * @param newStatus The new status for the service. + */ + function setServiceStatus(uint256 serviceId, ServiceStatus newStatus) + external + serviceExists(serviceId) + onlyServiceOwner(serviceId) + { + Service storage service = services[serviceId]; + ServiceStatus oldStatus = service.status; + + // Validate status transitions + if (newStatus == ServiceStatus.PUBLISHED) { + require(service.agentAddress != address(0), "Service must have agent to be published"); + require(oldStatus == ServiceStatus.DRAFT || oldStatus == ServiceStatus.ARCHIVED, + "Can only publish from DRAFT or ARCHIVED"); + } else if (newStatus == ServiceStatus.DELETED) { + revert("Invalid status transition"); + } else if (newStatus == ServiceStatus.ARCHIVED) { + require(oldStatus == ServiceStatus.PUBLISHED || oldStatus == ServiceStatus.DRAFT, + "Can only archive from PUBLISHED or DRAFT"); + } + + service.status = newStatus; + service.version++; + + emit ServiceStatusChanged(serviceId, oldStatus, newStatus); + } + /** - * @dev Retrieves the details of a service. - * @param name The name of the service to retrieve. - * @return The details of the service. + * @dev Soft deletes a service by setting its status to DELETED. + * @param serviceId The ID of the service to delete. */ - function getService(string memory name) external view returns (Service memory) { - return services[name]; + function deleteService(uint256 serviceId) + external + serviceExists(serviceId) + onlyServiceOwner(serviceId) + { + Service storage service = services[serviceId]; + require(service.status != ServiceStatus.DELETED, "Service already deleted"); + + ServiceStatus oldStatus = service.status; + service.status = ServiceStatus.DELETED; + + emit ServiceStatusChanged(serviceId, oldStatus, ServiceStatus.DELETED); } - function isServiceRegistered(string memory name) external view returns (bool) { - require(bytes(name).length > 0, "Invalid service name"); + // ============================================================================ + // Ownership Management + // ============================================================================ + + /** + * @dev Transfers service ownership to a new owner. + * @param serviceId The ID of the service. + * @param newOwner The new owner's address. + */ + function transferServiceOwnership(uint256 serviceId, address newOwner) + external + serviceExists(serviceId) + onlyServiceOwner(serviceId) + { + require(newOwner != address(0), "Invalid new owner"); + + Service storage service = services[serviceId]; + address oldOwner = service.owner; + + service.owner = newOwner; + + // Update owner mappings + _removeFromOwnerServices(oldOwner, serviceId); + servicesByOwner[newOwner].push(serviceId); + + emit ServiceOwnershipTransferred(serviceId, oldOwner, newOwner); + } - return bytes(services[name].name).length > 0; + /** + * @dev Gets the owner of a service. + * @param serviceId The ID of the service. + * @return The owner's address. + */ + function getServiceOwner(uint256 serviceId) external view serviceExists(serviceId) returns (address) { + return services[serviceId].owner; } - function updateService(string memory name, string memory category, string memory description) external onlyOwner { - require(this.isServiceRegistered(name), "Service not registered"); + // ============================================================================ + // Agent Assignment Management + // ============================================================================ - services[name] = Service({ - name: name, - category: category, - description: description - }); + /** + * @dev Assigns an agent to a service. + * @param serviceId The ID of the service. + * @param agent The agent's address. + */ + function assignAgentToService(uint256 serviceId, address agent) + external + serviceExists(serviceId) + onlyServiceOwner(serviceId) + { + require(agent != address(0), "Invalid agent address"); + + Service storage service = services[serviceId]; + address oldAgent = service.agentAddress; + + if (oldAgent != address(0)) { + _removeFromAgentServices(oldAgent, serviceId); + emit ServiceAgentUnassigned(serviceId, oldAgent); + } + + service.agentAddress = agent; + servicesByAgent[agent].push(serviceId); + + emit ServiceAgentAssigned(serviceId, agent); + } - emit ServiceUpdated(name, category, description); + /** + * @dev Unassigns the current agent from a service and sets the service status. + * @param serviceId The ID of the service. + * @param newStatus The new status for the service (DRAFT or ARCHIVED). + */ + function unassignAgentFromService(uint256 serviceId, ServiceStatus newStatus) + external + serviceExists(serviceId) + onlyServiceOwner(serviceId) + { + require(newStatus == ServiceStatus.DRAFT || newStatus == ServiceStatus.ARCHIVED, + "Status must be DRAFT or ARCHIVED"); + + Service storage service = services[serviceId]; + address oldAgent = service.agentAddress; + ServiceStatus oldStatus = service.status; + + require(oldAgent != address(0), "No agent assigned"); + + service.agentAddress = address(0); + service.status = newStatus; + service.version++; + + _removeFromAgentServices(oldAgent, serviceId); + + emit ServiceAgentUnassigned(serviceId, oldAgent); + emit ServiceStatusChanged(serviceId, oldStatus, newStatus); + } + + // ============================================================================ + // Query Functions + // ============================================================================ + + /** + * @dev Gets all services owned by an address. + * @param owner The owner's address. + * @return Array of service IDs owned by the address. + */ + function getServicesByOwner(address owner) external view returns (uint256[] memory) { + return servicesByOwner[owner]; + } + + /** + * @dev Gets all services assigned to an agent. + * @param agent The agent's address. + * @return Array of service IDs assigned to the agent. + */ + function getServicesByAgent(address agent) external view returns (uint256[] memory) { + return servicesByAgent[agent]; + } + + /** + * @dev Gets the total number of services. + * @return The total service count. + */ + function getTotalServiceCount() external view returns (uint256) { + return totalServices; + } + + // ============================================================================ + // Private Helper Functions + // ============================================================================ + + /** + * @dev Validates a service status transition. + * @param from Current status. + * @param to Desired new status. + * @return True if transition is valid. + */ + function _isValidStatusTransition(ServiceStatus from, ServiceStatus to) internal pure returns (bool) { + if (from == ServiceStatus.DRAFT) { + return to == ServiceStatus.PUBLISHED || to == ServiceStatus.DELETED; + } else if (from == ServiceStatus.PUBLISHED) { + return to == ServiceStatus.ARCHIVED || to == ServiceStatus.DELETED; + } else if (from == ServiceStatus.ARCHIVED) { + return to == ServiceStatus.PUBLISHED || to == ServiceStatus.DELETED; + } + return false; // DELETED status is final + } + + /** + * @dev Removes a service ID from an owner's services array. + * @param owner The owner's address. + * @param serviceId The service ID to remove. + */ + function _removeFromOwnerServices(address owner, uint256 serviceId) internal { + uint256[] storage ownerServices = servicesByOwner[owner]; + for (uint256 i = 0; i < ownerServices.length; i++) { + if (ownerServices[i] == serviceId) { + ownerServices[i] = ownerServices[ownerServices.length - 1]; + ownerServices.pop(); + break; + } + } + } + + /** + * @dev Removes a service ID from an agent's services array. + * @param agent The agent's address. + * @param serviceId The service ID to remove. + */ + function _removeFromAgentServices(address agent, uint256 serviceId) internal { + uint256[] storage agentServices = servicesByAgent[agent]; + for (uint256 i = 0; i < agentServices.length; i++) { + if (agentServices[i] == serviceId) { + agentServices[i] = agentServices[agentServices.length - 1]; + agentServices.pop(); + break; + } + } } /** diff --git a/packages/contracts/contracts/TaskRegistryUpgradeable.sol b/packages/contracts/contracts/TaskRegistryUpgradeable.sol deleted file mode 100644 index 59334d0..0000000 --- a/packages/contracts/contracts/TaskRegistryUpgradeable.sol +++ /dev/null @@ -1,196 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.22; - -import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; -import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; -import "./interfaces/IProposalStruct.sol"; -import "./AgentsRegistryUpgradeable.sol"; -import "./lib/TransferHelper.sol"; - -/** - * @title TaskRegistryUpgradeable - * @author leonprou - * @notice A smart contract that stores information about the tasks issued for the agent service providers. - * @dev Upgradeable version using UUPS proxy pattern - */ -contract TaskRegistryUpgradeable is Initializable, OwnableUpgradeable, UUPSUpgradeable, IProposalStruct { - - enum TaskStatus { CREATED, ASSIGNED, COMPLETED, CANCELED } - - struct TaskData { - uint256 id; - string prompt; - address issuer; - TaskStatus status; - address assignee; - uint256 proposalId; - string result; - uint8 rating; - } - - mapping(uint256 => TaskData) public tasks; - mapping(address => uint256[]) public issuerTasks; - uint256 private nextTaskId; - AgentsRegistryUpgradeable public agentRegistry; - - modifier onlyTaskIssuer(uint256 taskId) { - require(msg.sender == tasks[taskId].issuer, "Not the issuer of the task"); - _; - } - - /// @custom:oz-upgrades-unsafe-allow constructor - constructor() { - _disableInitializers(); - } - - /** - * @dev Initializes the contract - * @param _initialTaskId The starting task ID - * @param _agentRegistry The address of the agent registry - */ - function initialize( - uint256 _initialTaskId, - AgentsRegistryUpgradeable _agentRegistry - ) public initializer { - __Ownable_init(msg.sender); - __UUPSUpgradeable_init(); - - nextTaskId = _initialTaskId; - agentRegistry = _agentRegistry; - } - - event TaskCreated(address indexed issuer, address indexed assignee, uint256 taskId, uint256 proposalId, string prompt); - event TaskStatusChanged(uint256 indexed taskId, TaskStatus status); - event TaskAssigned(uint256 indexed taskId, address indexed agent); - event TaskCanceled(uint256 indexed taskId); - event ProposalApproved(uint256 indexed taskId, ServiceProposal proposal); - event TaskCompleted(uint256 indexed taskId, string result); - event TaskRated(uint256 indexed taskId, uint8 rating); - - /** - * @dev Creates a new task with the given prompt and task type. - * @param prompt The description or prompt of the task. - * @return taskId The ID of the newly created task. - */ - function createTask( - string memory prompt, - uint256 proposalId - ) external payable returns (TaskData memory) { - ServiceProposal memory proposal = agentRegistry.getProposal(proposalId); - require(proposal.issuer != address(0), "ServiceProposal not found"); - - if (proposal.tokenAddress == address(0)) { - require(proposal.price == msg.value, "Invalid price"); - } else { - require(msg.value == 0, "No ETH should be sent for ERC20 payments"); - TransferHelper.safeTransferFrom(proposal.tokenAddress, msg.sender, address(this), proposal.price); - } - - TaskData storage task = tasks[nextTaskId]; - task.id = nextTaskId; - task.prompt = prompt; - task.issuer = msg.sender; - task.proposalId = proposalId; - task.assignee = proposal.issuer; - issuerTasks[msg.sender].push(nextTaskId); - task.status = TaskStatus.ASSIGNED; - emit TaskCreated(msg.sender, proposal.issuer, nextTaskId, proposal.proposalId, prompt); - - nextTaskId++; - return task; - } - - /** - * @dev Completes a task with the given result. - * @param taskId The ID of the task. - * @param result The result or output of the completed task. - */ - function completeTask(uint256 taskId, string memory result) external { - TaskData storage task = tasks[taskId]; - require(msg.sender == task.assignee, "Not authorized"); - require(task.status == TaskStatus.ASSIGNED, "Invalid task status"); - - task.status = TaskStatus.COMPLETED; - task.result = result; - ServiceProposal memory proposal = agentRegistry.getProposal(task.proposalId); - - if (proposal.tokenAddress == address(0)) { - TransferHelper.safeTransferETH(task.issuer, proposal.price); - } else { - TransferHelper.safeTransfer(proposal.tokenAddress, task.issuer, proposal.price); - } - - emit TaskStatusChanged(taskId, task.status); - emit TaskCompleted(taskId, result); - } - - /** - * @dev Rated a completed task, called by the task issuer - * @param taskId The ID of the task. - * @param rating task rating from 0 to 100. - */ - function rateTask(uint256 taskId, uint8 rating) onlyTaskIssuer(taskId) external { - TaskData storage task = tasks[taskId]; - - require(task.status == TaskStatus.COMPLETED, "Task is not completed"); - require(task.rating == 0, "Task got rating already"); - require(rating >= 0 && rating <= 100, "Rating must be between 0 and 100"); - - task.rating = rating; - ServiceProposal memory proposal = agentRegistry.getProposal(task.proposalId); - - agentRegistry.addRating(proposal.issuer, rating); - - emit TaskRated(taskId, rating); - } - - /** - * @dev Cancels a task that is in ASSIGNED status and refunds the payment. - * @param taskId The ID of the task to cancel. - */ - function cancelTask(uint256 taskId) external onlyTaskIssuer(taskId) { - TaskData storage task = tasks[taskId]; - require(task.status != TaskStatus.COMPLETED && task.status != TaskStatus.CANCELED, "Task cannot be canceled"); - - task.status = TaskStatus.CANCELED; - ServiceProposal memory proposal = agentRegistry.getProposal(task.proposalId); - - // Refund the payment to the issuer - if (proposal.tokenAddress == address(0)) { - TransferHelper.safeTransferETH(task.issuer, proposal.price); - } else { - TransferHelper.safeTransfer(proposal.tokenAddress, task.issuer, proposal.price); - } - - emit TaskStatusChanged(taskId, task.status); - emit TaskCanceled(taskId); - } - - function getTasksByIssuer(address issuer) external view returns (uint256[] memory) { - return issuerTasks[issuer]; - } - - function getTask(uint256 taskId) external view returns (TaskData memory) { - return tasks[taskId]; - } - - function getStatus(uint256 taskId) external view returns (TaskStatus) { - return tasks[taskId].status; - } - - function getAssignee(uint256 taskId) external view returns (address) { - return tasks[taskId].assignee; - } - - /** - * @dev Function that should revert when `msg.sender` is not authorized to upgrade the contract. - * @param newImplementation Address of the new implementation - */ - function _authorizeUpgrade(address newImplementation) internal override onlyOwner {} - - /** - * @dev Storage gap for future upgrades - */ - uint256[50] private __gap; -} \ No newline at end of file diff --git a/packages/contracts/contracts/interfaces/IProposalStruct.sol b/packages/contracts/contracts/interfaces/IProposalStruct.sol deleted file mode 100644 index 01026c4..0000000 --- a/packages/contracts/contracts/interfaces/IProposalStruct.sol +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; -interface IProposalStruct { - struct ServiceProposal { - address issuer; - string serviceName; - uint256 price; - address tokenAddress; - uint256 proposalId; - bool isActive; - } -} \ No newline at end of file diff --git a/packages/contracts/ignition/modules/DeployAll.ts b/packages/contracts/ignition/modules/DeployAll.ts index 31bc26b..307e00c 100644 --- a/packages/contracts/ignition/modules/DeployAll.ts +++ b/packages/contracts/ignition/modules/DeployAll.ts @@ -2,16 +2,17 @@ import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; import EnsembleCreditsModule from "./EnsembleCredits"; import ServiceRegistryUpgradeableModule from "./ServiceRegistryUpgradeable"; import AgentsRegistryUpgradeableModule from "./AgentsRegistryUpgradeable"; -import TaskRegistryUpgradeableModule from "./TaskRegistryUpgradeable"; /** - * Master deployment module for the entire Ensemble Framework + * Master deployment module for the Ensemble Framework V2 * * This module orchestrates the deployment of all contracts in the correct order: * 1. EnsembleCredits (independent ERC20 token) - * 2. ServiceRegistry (base registry) - * 3. AgentsRegistry (depends on ServiceRegistry) - * 4. TaskRegistry (depends on ServiceRegistry and can integrate with EnsembleCredits) + * 2. ServiceRegistry (base registry for service management) + * 3. AgentsRegistry (agent management with reputation system) + * + * Note: TaskRegistry has been removed as it's now legacy. + * Services and agents are managed independently through their respective registries. * * Parameters (all optional & overridable via CLI / ignition.json): * - tokenName (string) : ERC20 name for credits (default: "Ensemble Credits") @@ -30,20 +31,11 @@ export default buildModule("DeployAllModule", (m) => { // Deploy ServiceRegistry (base dependency) const { serviceRegistry } = m.useModule(ServiceRegistryUpgradeableModule); - // Deploy AgentsRegistry (depends on ServiceRegistry) + // Deploy AgentsRegistry (depends on ServiceRegistry for V1 migration only) const { agentsRegistry, agentsRegistryProxy, agentsRegistryImpl } = m.useModule(AgentsRegistryUpgradeableModule); - - // Deploy TaskRegistry (depends on ServiceRegistry) - const { taskRegistry, taskRegistryProxy, taskRegistryImpl } = m.useModule(TaskRegistryUpgradeableModule); // Optional: Set up initial integrations between contracts // Note: These are commented out as they depend on your specific business logic - - // Example: Grant minter role to TaskRegistry for automatic reward distribution - // m.call(ensembleCredits, "grantRole", [ - // m.staticCall(ensembleCredits, "MINTER_ROLE"), - // taskRegistry - // ], { id: "GrantMinterRoleToTaskRegistry" }); return { // ERC20 Token @@ -52,14 +44,11 @@ export default buildModule("DeployAllModule", (m) => { // Registry Contracts (Proxy Instances) serviceRegistry, agentsRegistry, - taskRegistry, // Proxy Addresses (for upgrades) agentsRegistryProxy, - taskRegistryProxy, // Implementation Addresses (for verification) - agentsRegistryImpl, - taskRegistryImpl + agentsRegistryImpl }; -}); \ No newline at end of file +}); \ No newline at end of file diff --git a/packages/contracts/ignition/modules/TaskRegistryUpgradeable.ts b/packages/contracts/ignition/modules/TaskRegistryUpgradeable.ts deleted file mode 100644 index b25c7ef..0000000 --- a/packages/contracts/ignition/modules/TaskRegistryUpgradeable.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; -import AgentsRegistryUpgradeableModule from "./AgentsRegistryUpgradeable"; - -const TaskRegistryUpgradeableModule = buildModule("TaskRegistryUpgradeableModule", (m) => { - // Use the AgentsRegistryUpgradeable module (which includes ServiceRegistry) - const { agentsRegistry, serviceRegistry } = m.useModule(AgentsRegistryUpgradeableModule); - - // Deploy the TaskRegistryUpgradeable implementation - const taskRegistryImpl = m.contract("TaskRegistryUpgradeable", [], { - id: "TaskRegistryImpl" - }); - - // Get initialization parameters - const initialTaskId = m.getParameter("initialTaskId", 1); - - // Encode the initialize function call with required parameters - const initializeData = m.encodeFunctionCall(taskRegistryImpl, "initialize", [ - initialTaskId, - agentsRegistry - ]); - - // Deploy the UUPS proxy with the implementation and initialization data - const taskRegistryProxy = m.contract("ERC1967Proxy", [ - taskRegistryImpl, - initializeData, - ], { - id: "TaskRegistryProxy" - }); - - // Create a contract instance that uses the proxy address but the implementation ABI - const taskRegistry = m.contractAt("TaskRegistryUpgradeable", taskRegistryProxy, { - id: "TaskRegistryProxied" - }); - - return { - taskRegistry, - taskRegistryProxy, - taskRegistryImpl, - agentsRegistry, - serviceRegistry - }; -}); - -export default TaskRegistryUpgradeableModule; \ No newline at end of file diff --git a/packages/contracts/ignition/modules/UpgradeRegistries.ts b/packages/contracts/ignition/modules/UpgradeRegistries.ts index 7aba17d..013fdd1 100644 --- a/packages/contracts/ignition/modules/UpgradeRegistries.ts +++ b/packages/contracts/ignition/modules/UpgradeRegistries.ts @@ -14,7 +14,6 @@ const UpgradeRegistriesModule = buildModule("UpgradeRegistriesModule", (m) => { // Get existing proxy addresses (should be provided as parameters) const serviceRegistryProxyAddress = m.getParameter("serviceRegistryProxy"); const agentsRegistryProxyAddress = m.getParameter("agentsRegistryProxy"); - const taskRegistryProxyAddress = m.getParameter("taskRegistryProxy"); // Deploy new implementation contracts const newServiceRegistryImpl = m.contract("ServiceRegistryUpgradeable", [], { @@ -25,9 +24,6 @@ const UpgradeRegistriesModule = buildModule("UpgradeRegistriesModule", (m) => { id: "NewAgentsRegistryImpl" }); - const newTaskRegistryImpl = m.contract("TaskRegistryUpgradeable", [], { - id: "NewTaskRegistryImpl" - }); // Get contract instances for existing proxies const serviceRegistryProxy = m.contractAt("ServiceRegistryUpgradeable", serviceRegistryProxyAddress, { @@ -38,9 +34,6 @@ const UpgradeRegistriesModule = buildModule("UpgradeRegistriesModule", (m) => { id: "ExistingAgentsRegistryProxy" }); - const taskRegistryProxy = m.contractAt("TaskRegistryUpgradeable", taskRegistryProxyAddress, { - id: "ExistingTaskRegistryProxy" - }); // Perform upgrades by calling upgradeToAndCall on each proxy with empty data // Note: Only the proxy owner can perform these upgrades @@ -53,17 +46,12 @@ const UpgradeRegistriesModule = buildModule("UpgradeRegistriesModule", (m) => { id: "UpgradeAgentsRegistry" }); - m.call(taskRegistryProxy, "upgradeToAndCall", [newTaskRegistryImpl, "0x"], { - id: "UpgradeTaskRegistry" - }); return { newServiceRegistryImpl, newAgentsRegistryImpl, - newTaskRegistryImpl, serviceRegistryProxy, - agentsRegistryProxy, - taskRegistryProxy + agentsRegistryProxy }; }); diff --git a/packages/contracts/ignition/params/baseSepolia.json b/packages/contracts/ignition/params/baseSepolia.json index 5fbf0a8..e694c20 100644 --- a/packages/contracts/ignition/params/baseSepolia.json +++ b/packages/contracts/ignition/params/baseSepolia.json @@ -1,8 +1,5 @@ { "AgentsRegistryUpgradeableModule": { "v1RegistryAddress": "0x0000000000000000000000000000000000000000" - }, - "TaskRegistryUpgradeableModule": { - "initialTaskId": 1 } -} \ No newline at end of file +} \ No newline at end of file diff --git a/packages/contracts/ignition/params/local.json b/packages/contracts/ignition/params/local.json index 5fbf0a8..e694c20 100644 --- a/packages/contracts/ignition/params/local.json +++ b/packages/contracts/ignition/params/local.json @@ -1,8 +1,5 @@ { "AgentsRegistryUpgradeableModule": { "v1RegistryAddress": "0x0000000000000000000000000000000000000000" - }, - "TaskRegistryUpgradeableModule": { - "initialTaskId": 1 } -} \ No newline at end of file +} \ No newline at end of file diff --git a/packages/contracts/scripts/create-task.ts b/packages/contracts/scripts/create-task.ts deleted file mode 100644 index 67fc3ee..0000000 --- a/packages/contracts/scripts/create-task.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { TaskRegistryUpgradeable } from "../typechain-types"; -require('dotenv').config('../.env'); - -const hre = require("hardhat"); - -async function main() { - const taskRegistryAddress = process.env.TASK_REGISTRY_ADDRESS; - console.log("Task Registry Address:", taskRegistryAddress); - // Get the contract factory and deployer - const [deployer] = await hre.ethers.getSigners(); - - console.log("account address:", deployer.address); - - // Get the deployed contract instance - const TaskRegistry = await hre.ethers.getContractFactory("TaskRegistryUpgradeable"); - const taskRegistry = await TaskRegistry.attach(taskRegistryAddress) as TaskRegistryUpgradeable; - - try { - // Create a task without ignition - const simpleTask = await taskRegistry.createTask( - "Do X for me", - 0 - ); - // Wait for the transaction to be mined - const simpleTaskReceipt = await simpleTask.wait(); - console.log("Simple task created in tx:", simpleTaskReceipt?.hash); - - - // const secondTask = await taskRegistry.createTask( - // "Do Y for me", - // 0 - // ); - // Wait for the transaction to be mined - // const secondTaskReceipt = await secondTask.wait(); - // console.log("Simple task created in tx:", secondTaskReceipt.hash); - - } catch (error) { - console.error("Error:", error); - } -} - -main() - .then(() => process.exit(0)) - .catch((error) => { - console.error(error); - process.exit(1); - }); \ No newline at end of file diff --git a/packages/contracts/scripts/deploy.js b/packages/contracts/scripts/deploy.js index 96a3bdd..1acc09e 100644 --- a/packages/contracts/scripts/deploy.js +++ b/packages/contracts/scripts/deploy.js @@ -29,14 +29,8 @@ async function main() { await agentsRegistry.waitForDeployment(); console.log("AgentsRegistryUpgradeable deployed to:", await agentsRegistry.getAddress()); - // Deploy TaskRegistryUpgradeable - const TaskRegistry = await ethers.getContractFactory("TaskRegistryUpgradeable"); - const taskRegistry = await upgrades.deployProxy(TaskRegistry, [1, await agentsRegistry.getAddress()], { - initializer: "initialize", - kind: "uups" - }); - await taskRegistry.waitForDeployment(); - console.log("TaskRegistryUpgradeable deployed to:", await taskRegistry.getAddress()); + console.log("\nNote: TaskRegistry has been removed as it's now legacy."); + console.log("Services and agents are managed independently through their respective registries."); } main() diff --git a/packages/contracts/scripts/deploy.ts b/packages/contracts/scripts/deploy.ts index 7ecf0d0..6ec2d47 100644 --- a/packages/contracts/scripts/deploy.ts +++ b/packages/contracts/scripts/deploy.ts @@ -37,34 +37,13 @@ async function main(): Promise { const agentRegistryAddress = await agentsRegistry.getAddress(); console.log(`AGENT_REGISTRY_ADDRESS=${agentRegistryAddress}`); - // Deploy TaskRegistryUpgradeable (depends on AgentsRegistry) - console.log("Deploying TaskRegistryUpgradeable..."); - const TaskRegistry = await hre.ethers.getContractFactory("TaskRegistryUpgradeable"); - const taskRegistry = await upgrades.deployProxy(TaskRegistry, [1, agentRegistryAddress], { - initializer: "initialize", - kind: "uups" - }); - await taskRegistry.waitForDeployment(); - const taskRegistryAddress = await taskRegistry.getAddress(); - console.log(`TASK_REGISTRY_ADDRESS=${taskRegistryAddress}`); - console.log("\n=== Deployment Summary ==="); console.log(`ServiceRegistry: ${serviceRegistryAddress}`); console.log(`AgentsRegistry: ${agentRegistryAddress}`); - console.log(`TaskRegistry: ${taskRegistryAddress}`); console.log(`Mock V1 Registry: ${mockV1Address}`); - - // Uncomment the lines below if you want to create tasks for testing - /* - const tx1 = await taskRegistry.createTask("Do X for me", 0); - console.log(`First task created in tx: ${tx1.hash}`); - - const tx2 = await taskRegistry.createTask("Do Y for me", 0); - console.log(`Second task created in tx: ${tx2.hash}`); - - const tasks = await taskRegistry.getTasksByOwner(deployer.address); - console.log("Tasks created by deployer:", tasks); - */ + + console.log("\nNote: TaskRegistry has been removed as it's now legacy."); + console.log("Services and agents are managed independently through their respective registries."); } // Error handling and process exit @@ -76,4 +55,4 @@ main() .catch((error) => { console.error("Error during deployment:", error); process.exit(1); - }); + }); \ No newline at end of file diff --git a/packages/contracts/test/AgentRegistry.test.js b/packages/contracts/test/AgentRegistry.test.js index de2b732..18a9a11 100644 --- a/packages/contracts/test/AgentRegistry.test.js +++ b/packages/contracts/test/AgentRegistry.test.js @@ -1,750 +1,424 @@ const { expect } = require("chai"); const { ethers, upgrades } = require("hardhat"); -const AgentRegistryV1Artifact = require('./artifacts/AgentsRegistryV1.json') -describe("AgentsRegistryUpgradeable", function () { - let AgentRegistry; - let agentRegistryV1 - let registry; +describe("AgentsRegistryUpgradeable V2 - Without Proposals", function () { + let AgentsRegistry; + let ServiceRegistry; + let agentsRegistry; let serviceRegistry; - let serviceRegistryV1; - let admin, agentOwner, agentAddress; - let agentUri = "https://ipfs.io/ipfs/bafkreigzpb44ndvlsfazfymmf6yvquoregceik56vyskf7e35joel7yati"; + let mockV1Registry; + let owner, addr1, addr2, agentAddr1, agentAddr2; + + // Test data constants + const AGENT_NAME_1 = "Test Agent 1"; + const AGENT_NAME_2 = "Test Agent 2"; + const AGENT_URI_1 = "ipfs://QmAgent1Hash/metadata.json"; + const AGENT_URI_2 = "ipfs://QmAgent2Hash/metadata.json"; + const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; beforeEach(async function () { - [admin, agentOwner, agentAddress, eveAddress] = await ethers.getSigners(); + [owner, addr1, addr2, agentAddr1, agentAddr2] = await ethers.getSigners(); - // Deploy ServiceRegistryUpgradeable - const ServiceRegistry = await ethers.getContractFactory("ServiceRegistryUpgradeable"); + // Deploy ServiceRegistry first (dependency) + ServiceRegistry = await ethers.getContractFactory("ServiceRegistryUpgradeable"); serviceRegistry = await upgrades.deployProxy(ServiceRegistry, [], { initializer: "initialize", kind: "uups" }); + await serviceRegistry.waitForDeployment(); - // Deploy legacy ServiceRegistry for V1 compatibility - const ServiceRegistryV1 = await ethers.getContractFactory("ServiceRegistryUpgradeable"); - serviceRegistryV1 = await upgrades.deployProxy(ServiceRegistryV1, [], { + // Deploy mock V1 registry for migration compatibility + mockV1Registry = await upgrades.deployProxy(ServiceRegistry, [], { initializer: "initialize", kind: "uups" }); + await mockV1Registry.waitForDeployment(); + + // Deploy AgentsRegistry + AgentsRegistry = await ethers.getContractFactory("AgentsRegistryUpgradeable"); + agentsRegistry = await upgrades.deployProxy( + AgentsRegistry, + [await mockV1Registry.getAddress(), await serviceRegistry.getAddress()], + { + initializer: "initialize", + kind: "uups" + } + ); + await agentsRegistry.waitForDeployment(); + }); - const AgentRegistryV1 = await ethers.getContractFactoryFromArtifact(AgentRegistryV1Artifact); - agentRegistryV1 = await AgentRegistryV1.deploy(serviceRegistryV1.target); - - // Deploy AgentsRegistryUpgradeable - AgentRegistry = await ethers.getContractFactory("AgentsRegistryUpgradeable"); - registry = await upgrades.deployProxy(AgentRegistry, [agentRegistryV1.target, serviceRegistry.target], { - initializer: "initialize", - kind: "uups" + describe("1. Contract Initialization & Deployment", function () { + it("Should initialize with correct initial state", async function () { + expect(await agentsRegistry.owner()).to.equal(owner.address); + expect(await agentsRegistry.agentRegistryV1()).to.equal(await mockV1Registry.getAddress()); + expect(await agentsRegistry.serviceRegistry()).to.equal(await serviceRegistry.getAddress()); }); - }); - describe('#Setters', () => { - it("should set the address of the TaskRegistry contract", async function () { - const newTaskRegistryAddress = agentAddress; - await registry.connect(admin).setTaskRegistry(newTaskRegistryAddress); - expect(await registry.taskRegistry()).to.equal(newTaskRegistryAddress); + it("Should be deployed as UUPS proxy", async function () { + const implementationAddress = await upgrades.erc1967.getImplementationAddress( + await agentsRegistry.getAddress() + ); + expect(implementationAddress).to.not.equal(ZERO_ADDRESS); }); - it("should set the address of the ServiceRegistry contract", async function () { - const newServiceRegistryAddress = agentAddress; - await registry.connect(admin).setServiceRegistry(newServiceRegistryAddress); - expect(await registry.serviceRegistry()).to.equal(newServiceRegistryAddress); + it("Should prevent multiple initialization", async function () { + await expect( + agentsRegistry.initialize(await mockV1Registry.getAddress(), await serviceRegistry.getAddress()) + ).to.be.reverted; }); - }) + }); - describe('#registerAgent', () => { - it("Should register a new agent without proposal", async function () { - const request = registry.connect(agentOwner).registerAgent( - agentAddress, - "Simple Agent", - agentUri + describe("2. Agent Registration", function () { + it("Should register a new agent successfully", async function () { + const tx = await agentsRegistry.connect(addr1).registerAgent( + agentAddr1.address, + AGENT_NAME_1, + AGENT_URI_1 ); - await expect(request) - .to.emit(registry, "AgentRegistered") - .withArgs(agentAddress, agentOwner, "Simple Agent", agentUri); - - const agentData = await registry.getAgentData(agentAddress); + await expect(tx) + .to.emit(agentsRegistry, "AgentRegistered") + .withArgs(agentAddr1.address, addr1.address, AGENT_NAME_1, AGENT_URI_1); - expect(agentData.name).to.equal("Simple Agent"); - expect(agentData.agentUri).to.equal(agentUri); - expect(agentData.owner).to.equal(agentOwner.address); - expect(agentData.agent).to.equal(agentAddress); + const agentData = await agentsRegistry.getAgentData(agentAddr1.address); + expect(agentData.name).to.equal(AGENT_NAME_1); + expect(agentData.agentUri).to.equal(AGENT_URI_1); + expect(agentData.owner).to.equal(addr1.address); + expect(agentData.agent).to.equal(agentAddr1.address); expect(agentData.reputation).to.equal(0); expect(agentData.totalRatings).to.equal(0); }); - it("Should not register the same agent twice", async function () { - await registry.connect(agentOwner).registerAgent( - agentAddress, - "Simple Agent", - agentUri + it("Should fail to register an agent twice", async function () { + await agentsRegistry.connect(addr1).registerAgent( + agentAddr1.address, + AGENT_NAME_1, + AGENT_URI_1 ); await expect( - registry.connect(agentOwner).registerAgent( - agentAddress, - "Another Agent", - agentUri + agentsRegistry.connect(addr2).registerAgent( + agentAddr1.address, + AGENT_NAME_2, + AGENT_URI_2 ) ).to.be.revertedWith("Agent already registered"); }); - it("Should handle empty name parameter", async function () { - const request = registry.connect(agentOwner).registerAgent( - agentAddress, - "", - agentUri - ); - - await expect(request) - .to.emit(registry, "AgentRegistered") - .withArgs(agentAddress, agentOwner, "", agentUri); - }); - - it("Should handle empty agentUri parameter", async function () { - const request = registry.connect(agentOwner).registerAgent( - agentAddress, - "Simple Agent", - "" - ); - - await expect(request) - .to.emit(registry, "AgentRegistered") - .withArgs(agentAddress, agentOwner, "Simple Agent", ""); - }); - - it("Should allow multiple agents registered by the same owner", async function () { - const [, , secondAgent] = await ethers.getSigners(); - - await registry.connect(agentOwner).registerAgent( - agentAddress, - "First Agent", - agentUri + it("Should allow different owners to register different agents", async function () { + await agentsRegistry.connect(addr1).registerAgent( + agentAddr1.address, + AGENT_NAME_1, + AGENT_URI_1 ); - await registry.connect(agentOwner).registerAgent( - eveAddress, - "Second Agent", - agentUri + await agentsRegistry.connect(addr2).registerAgent( + agentAddr2.address, + AGENT_NAME_2, + AGENT_URI_2 ); - const firstAgentData = await registry.getAgentData(agentAddress); - const secondAgentData = await registry.getAgentData(eveAddress); + const agent1Data = await agentsRegistry.getAgentData(agentAddr1.address); + const agent2Data = await agentsRegistry.getAgentData(agentAddr2.address); - expect(firstAgentData.owner).to.equal(agentOwner.address); - expect(secondAgentData.owner).to.equal(agentOwner.address); - expect(firstAgentData.name).to.equal("First Agent"); - expect(secondAgentData.name).to.equal("Second Agent"); + expect(agent1Data.owner).to.equal(addr1.address); + expect(agent2Data.owner).to.equal(addr2.address); }); + }); - it("Should handle agent address same as owner address", async function () { - await registry.connect(agentOwner).registerAgent( - agentOwner.address, - "Self Agent", - agentUri + describe("3. Agent Data Management", function () { + beforeEach(async function () { + await agentsRegistry.connect(addr1).registerAgent( + agentAddr1.address, + AGENT_NAME_1, + AGENT_URI_1 ); - - const agentData = await registry.getAgentData(agentOwner.address); - expect(agentData.owner).to.equal(agentOwner.address); - expect(agentData.agent).to.equal(agentOwner.address); }); - it("Should handle very long strings", async function () { - const longName = "A".repeat(1000); - const longUri = "https://".concat("very-long-uri-".repeat(100)); + it("Should update agent data by owner", async function () { + const newName = "Updated Agent Name"; + const newUri = "ipfs://QmUpdatedHash/metadata.json"; - await registry.connect(agentOwner).registerAgent( - agentAddress, - longName, - longUri + const tx = await agentsRegistry.connect(addr1).setAgentData( + agentAddr1.address, + newName, + newUri ); - const agentData = await registry.getAgentData(agentAddress); - expect(agentData.name).to.equal(longName); - expect(agentData.agentUri).to.equal(longUri); - }); + await expect(tx) + .to.emit(agentsRegistry, "AgentDataUpdated") + .withArgs(agentAddr1.address, newName, newUri); - it("Should not create any proposals when using registerAgent", async function () { - await registry.connect(agentOwner).registerAgent( - agentAddress, - "Simple Agent", - agentUri - ); + const agentData = await agentsRegistry.getAgentData(agentAddr1.address); + expect(agentData.name).to.equal(newName); + expect(agentData.agentUri).to.equal(newUri); + }); - // Since proposals start at ID 1, checking if proposal 1 exists should return default values - const proposal = await registry.getProposal(1); - expect(proposal.issuer).to.equal(ethers.ZeroAddress); - expect(proposal.serviceName).to.equal(""); - expect(proposal.price).to.equal(0); - expect(proposal.proposalId).to.equal(0); - expect(proposal.isActive).to.equal(false); - }); - }) - - describe('#registerAgentWithService', () => { - this.beforeEach(async function () { - await serviceRegistry.registerService("Service1", "Category1", "Description1"); - }) - it("Should not register an agent if the service is not registered", async function () { + it("Should fail to update agent data by non-owner", async function () { await expect( - registry.connect(agentOwner).registerAgentWithService( - agentAddress, - "Service Agent", - agentUri, - "NonExistentService", - ethers.parseEther("0.01"), - ethers.ZeroAddress + agentsRegistry.connect(addr2).setAgentData( + agentAddr1.address, + "New Name", + "New URI" ) - ).to.be.revertedWith("Service not registered"); + ).to.be.revertedWith("Not the owner of the agent"); }); - it("Should register new agent", async function () { - const request = registry.connect(agentOwner).registerAgentWithService( - agentAddress, - "Service Agent", - agentUri, - "Service1", - ethers.parseEther("0.01"), - ethers.ZeroAddress - ); - - await expect(request) - .to.emit(registry, "AgentRegistered") - .withArgs(agentAddress, agentOwner, "Service Agent", agentUri) - .to.emit(registry, "ProposalAdded") - .withArgs(agentAddress, 1, "Service1", ethers.parseEther("0.01"), ethers.ZeroAddress); - - const agentData = await registry.getAgentData(agentAddress); - - expect(agentData.name).to.equal("Service Agent"); - expect(agentData.agentUri).to.equal(agentUri); - expect(agentData.owner).to.equal(agentOwner.address); - expect(agentData.agent).to.equal(agentAddress); - expect(agentData.reputation).to.equal(0); - - const proposalId = 1; - const proposal = await registry.getProposal(proposalId); - expect(proposal.issuer).to.equal(agentAddress); - expect(proposal.serviceName).to.equal("Service1"); - expect(proposal.price).to.equal(ethers.parseEther("0.01")); - expect(proposal.proposalId).to.equal(proposalId); - }); - - it("Should not register the same agent twice", async function () { - await registry.connect(agentOwner).registerAgentWithService( - agentAddress, - "Service Agent", - agentUri, - "Service1", - ethers.parseEther("0.01"), - ethers.ZeroAddress - ); - + it("Should fail to update non-existent agent", async function () { await expect( - registry.connect(agentOwner).registerAgentWithService( - agentAddress, - "Service Agent", - agentUri, - "Service1", - ethers.parseEther("0.01"), - ethers.ZeroAddress + agentsRegistry.connect(addr1).setAgentData( + agentAddr2.address, + "New Name", + "New URI" ) - ).to.be.revertedWith("Agent already registered"); + ).to.be.revertedWith("Not the owner of the agent"); }); - }) - - - describe('#Proposals', () => { + }); + describe("4. Agent Reputation System", function () { beforeEach(async function () { - await registry.connect(agentOwner).registerAgentWithService( - agentAddress, - "Service Agent", - agentUri, - "Service1", - ethers.parseEther("0.01"), - ethers.ZeroAddress + await agentsRegistry.connect(addr1).registerAgent( + agentAddr1.address, + AGENT_NAME_1, + AGENT_URI_1 ); }); + it("Should add rating to agent", async function () { + const rating = 85; + const tx = await agentsRegistry.connect(addr2).addRating(agentAddr1.address, rating); - it("Should add a proposal", async function () { - await registry.connect(agentOwner).addProposal(agentAddress, "Service1", ethers.parseEther("0.02"), ethers.ZeroAddress); - const proposalId = 2 - expect(await registry.getProposal(proposalId)).to.deep.equal([ - agentAddress.address, - "Service1", - ethers.parseEther("0.02"), - ethers.ZeroAddress, - proposalId, - true - ]); - }); - - it("Should not add a proposal if service is not registered", async function () { - await expect(registry.connect(agentOwner).addProposal(agentAddress, "NonExistentService", ethers.parseEther("0.02"), ethers.ZeroAddress)) - .to.be.revertedWith("Service not registered"); - }); - - it("Should remove a proposal", async function () { - - await registry.connect(agentOwner).addProposal(agentAddress, "Service1", ethers.parseEther("0.02"), ethers.ZeroAddress); - - await registry.connect(agentOwner).removeProposal(agentAddress, 2); - - expect(await registry.getProposal(2)).to.deep.equal([ - ethers.ZeroAddress, - "", - 0, - ethers.ZeroAddress, - 0, - false - ]); + await expect(tx) + .to.emit(agentsRegistry, "ReputationUpdated") + .withArgs(agentAddr1.address, rating); + const reputation = await agentsRegistry.getReputation(agentAddr1.address); + expect(reputation).to.equal(rating); - await registry.connect(agentOwner).removeProposal(agentAddress, 1); - - expect(await registry.getProposal(1)).to.deep.equal([ - ethers.ZeroAddress, - "", - 0, - ethers.ZeroAddress, - 0, - false - ]); + const agentData = await agentsRegistry.getAgentData(agentAddr1.address); + expect(agentData.totalRatings).to.equal(1); + + // Check that the user has rated + expect(await agentsRegistry.hasUserRated(agentAddr1.address, addr2.address)).to.be.true; }); - }); - - describe('#Reputation', () => { - - beforeEach(async () => { - await registry.connect(agentOwner).registerAgentWithService( - agentAddress, - "Service Agent", - agentUri, - "Service1", - ethers.parseEther("0.01"), - ethers.ZeroAddress - ); - }) - it("Should only allow taskRegistry to add a rating", async function () { - // Attempt to add a rating from a different address - await expect(registry.connect(agentOwner).addRating(agentAddress, 50)).to.be.revertedWith("Not the TaskRegistry contract"); - - await registry.connect(admin).setTaskRegistry(admin); - - await expect(registry.connect(admin).addRating(agentAddress, 50)).to.not.be.reverted; + it("Should prevent duplicate ratings from same user", async function () { + // First rating should succeed + await agentsRegistry.connect(addr1).addRating(agentAddr1.address, 80); + + // Second rating from same user should fail + await expect( + agentsRegistry.connect(addr1).addRating(agentAddr1.address, 90) + ).to.be.revertedWith("User has already rated this agent"); + + // Check that only one rating was recorded + const agentData = await agentsRegistry.getAgentData(agentAddr1.address); + expect(agentData.totalRatings).to.equal(1); + expect(agentData.reputation).to.equal(80); }); - it("Should add a rating", async function () { - await registry.connect(admin).setTaskRegistry(admin); + it("Should calculate average reputation correctly with different users", async function () { + // Different users rating the same agent + await agentsRegistry.connect(addr1).addRating(agentAddr1.address, 80); + await agentsRegistry.connect(addr2).addRating(agentAddr1.address, 90); + await agentsRegistry.connect(owner).addRating(agentAddr1.address, 100); - await expect(registry.connect(admin).addRating(agentAddress, 50)) - .to.emit(registry, "ReputationUpdated") - .withArgs(agentAddress, 50); - let agentData = await registry.getAgentData(agentAddress); - expect(agentData.reputation).to.equal(50); - expect(agentData.totalRatings).to.equal(1); - expect(await registry.getReputation(agentAddress)).to.equal(50); - - await expect(registry.connect(admin).addRating(agentAddress, 100)) - .to.emit(registry, "ReputationUpdated") - .withArgs(agentAddress, 75); - - agentData = await registry.getAgentData(agentAddress); - expect(agentData.reputation).to.equal(75); - expect(agentData.totalRatings).to.equal(2); + const reputation = await agentsRegistry.getReputation(agentAddr1.address); + expect(reputation).to.equal(90); // (80 + 90 + 100) / 3 = 90 - await expect(registry.connect(admin).addRating(agentAddress, 30)) - .to.emit(registry, "ReputationUpdated") - .withArgs(agentAddress, 60); - - agentData = await registry.getAgentData(agentAddress); - expect(agentData.reputation).to.equal(60); + const agentData = await agentsRegistry.getAgentData(agentAddr1.address); expect(agentData.totalRatings).to.equal(3); - }) - }) - - describe('#MigrateAgent', () => { - this.beforeEach(async function () { - await serviceRegistryV1.registerService("Service1", "Category1", "Description1"); - }) - - // FIXME: figure out why getAgentData is not returning the correct data - it("Should migrate an agent to a new registry", async function () { - await agentRegistryV1.connect(agentOwner).registerAgent( - agentAddress, - "Service Agent", - agentUri, - "Service1", - ethers.parseEther("0.01") - ); + }); + it("Should reject rating for non-existent agent", async function () { await expect( - registry.connect(eveAddress).migrateAgent(agentAddress) - ).to.be.revertedWith("Not owner or agent owner"); + agentsRegistry.connect(addr1).addRating(agentAddr2.address, 80) + ).to.be.revertedWith("Agent not registered"); + }); - await registry.migrateAgent(agentAddress) + it("Should reject invalid ratings", async function () { + await expect( + agentsRegistry.connect(addr2).addRating(agentAddr1.address, 101) + ).to.be.revertedWith("Rating must be between 0 and 100"); + }); - const agentData = await registry.getAgentData(agentAddress); + it("Should handle edge case ratings", async function () { + await agentsRegistry.connect(addr1).addRating(agentAddr1.address, 0); + let reputation = await agentsRegistry.getReputation(agentAddr1.address); + expect(reputation).to.equal(0); - expect(agentData.name).to.equal("Service Agent"); - expect(agentData.agentUri).to.equal(agentUri); - expect(agentData.owner).to.equal(agentOwner.address); - expect(agentData.agent).to.equal(agentAddress); - expect(agentData.reputation).to.equal(0); - expect(agentData.totalRatings).to.equal(0); + await agentsRegistry.connect(addr2).addRating(agentAddr1.address, 100); + reputation = await agentsRegistry.getReputation(agentAddr1.address); + expect(reputation).to.equal(50); // (0 + 100) / 2 = 50 }); - }) - describe('#SetAgentData', () => { - const newAgentName = "Updated Agent Name"; - const newAgentUri = "https://ipfs.io/ipfs/updated-hash"; + it("Should track rating status correctly", async function () { + // Check initial state + expect(await agentsRegistry.hasUserRated(agentAddr1.address, addr1.address)).to.be.false; + expect(await agentsRegistry.hasUserRated(agentAddr1.address, addr2.address)).to.be.false; + + // Add rating from addr1 + await agentsRegistry.connect(addr1).addRating(agentAddr1.address, 75); + + // Check updated state + expect(await agentsRegistry.hasUserRated(agentAddr1.address, addr1.address)).to.be.true; + expect(await agentsRegistry.hasUserRated(agentAddr1.address, addr2.address)).to.be.false; + + // Add rating from addr2 + await agentsRegistry.connect(addr2).addRating(agentAddr1.address, 85); + + // Check final state + expect(await agentsRegistry.hasUserRated(agentAddr1.address, addr1.address)).to.be.true; + expect(await agentsRegistry.hasUserRated(agentAddr1.address, addr2.address)).to.be.true; + }); + }); + describe("5. Agent Removal", function () { beforeEach(async function () { - await registry.connect(agentOwner).registerAgentWithService( - agentAddress, - "Service Agent", - agentUri, - "Service1", - ethers.parseEther("0.01"), - ethers.ZeroAddress + await agentsRegistry.connect(addr1).registerAgent( + agentAddr1.address, + AGENT_NAME_1, + AGENT_URI_1 ); }); - it("Should successfully update agent data", async function () { - const updateTx = registry.connect(agentOwner).setAgentData( - agentAddress, - newAgentName, - newAgentUri - ); + it("Should remove agent by owner", async function () { + const tx = await agentsRegistry.connect(addr1).removeAgent(agentAddr1.address); - await expect(updateTx) - .to.emit(registry, "AgentDataUpdated") - .withArgs(agentAddress, newAgentName, newAgentUri); + await expect(tx) + .to.emit(agentsRegistry, "AgentRemoved") + .withArgs(agentAddr1.address, addr1.address); - const agentData = await registry.getAgentData(agentAddress); - expect(agentData.name).to.equal(newAgentName); - expect(agentData.agentUri).to.equal(newAgentUri); - - // Verify other fields remain unchanged - expect(agentData.owner).to.equal(agentOwner.address); - expect(agentData.agent).to.equal(agentAddress); - expect(agentData.reputation).to.equal(0); - expect(agentData.totalRatings).to.equal(0); + const agentData = await agentsRegistry.getAgentData(agentAddr1.address); + expect(agentData.agent).to.equal(ZERO_ADDRESS); + expect(agentData.name).to.equal(""); }); - it("Should not allow non-owner to update agent data", async function () { - const [, , , unauthorizedUser] = await ethers.getSigners(); - + it("Should fail to remove agent by non-owner", async function () { await expect( - registry.connect(unauthorizedUser).setAgentData( - agentAddress, - newAgentName, - newAgentUri - ) + agentsRegistry.connect(addr2).removeAgent(agentAddr1.address) ).to.be.revertedWith("Not the owner of the agent"); }); - it("Should not allow updating unregistered agent", async function () { - const [, , , , unregisteredAgent] = await ethers.getSigners(); - + it("Should fail to remove non-existent agent", async function () { await expect( - registry.connect(unregisteredAgent).setAgentData( - unregisteredAgent.address, - newAgentName, - newAgentUri - ) + agentsRegistry.connect(addr1).removeAgent(agentAddr2.address) ).to.be.revertedWith("Not the owner of the agent"); }); - it("Should allow updating with empty strings", async function () { - const emptyName = ""; - const emptyUri = ""; + it("Should allow re-registration after removal", async function () { + await agentsRegistry.connect(addr1).removeAgent(agentAddr1.address); - const updateTx = registry.connect(agentOwner).setAgentData( - agentAddress, - emptyName, - emptyUri + // Should be able to register the same agent address again + await agentsRegistry.connect(addr2).registerAgent( + agentAddr1.address, + AGENT_NAME_2, + AGENT_URI_2 ); - await expect(updateTx) - .to.emit(registry, "AgentDataUpdated") - .withArgs(agentAddress, emptyName, emptyUri); - - const agentData = await registry.getAgentData(agentAddress); - expect(agentData.name).to.equal(emptyName); - expect(agentData.agentUri).to.equal(emptyUri); + const agentData = await agentsRegistry.getAgentData(agentAddr1.address); + expect(agentData.owner).to.equal(addr2.address); + expect(agentData.name).to.equal(AGENT_NAME_2); }); + }); - it("Should allow updating only name", async function () { - const originalAgentData = await registry.getAgentData(agentAddress); - - await registry.connect(agentOwner).setAgentData( - agentAddress, - newAgentName, - originalAgentData.agentUri // Keep original URI - ); + describe("6. Service Registry Management", function () { + it("Should set service registry by owner", async function () { + const newServiceRegistry = await upgrades.deployProxy(ServiceRegistry, [], { + initializer: "initialize", + kind: "uups" + }); + await newServiceRegistry.waitForDeployment(); - const updatedAgentData = await registry.getAgentData(agentAddress); - expect(updatedAgentData.name).to.equal(newAgentName); - expect(updatedAgentData.agentUri).to.equal(originalAgentData.agentUri); - }); - - it("Should allow updating only URI", async function () { - const originalAgentData = await registry.getAgentData(agentAddress); - - await registry.connect(agentOwner).setAgentData( - agentAddress, - originalAgentData.name, // Keep original name - newAgentUri + await agentsRegistry.connect(owner).setServiceRegistry( + await newServiceRegistry.getAddress() ); - const updatedAgentData = await registry.getAgentData(agentAddress); - expect(updatedAgentData.name).to.equal(originalAgentData.name); - expect(updatedAgentData.agentUri).to.equal(newAgentUri); - }); - - it("Should preserve reputation and ratings after update", async function () { - // Add some reputation first - await registry.connect(admin).setTaskRegistry(admin); - await registry.connect(admin).addRating(agentAddress, 80); - - const originalAgentData = await registry.getAgentData(agentAddress); - expect(originalAgentData.reputation).to.equal(80); - expect(originalAgentData.totalRatings).to.equal(1); - - // Update agent data - await registry.connect(agentOwner).setAgentData( - agentAddress, - newAgentName, - newAgentUri + expect(await agentsRegistry.serviceRegistry()).to.equal( + await newServiceRegistry.getAddress() ); - - // Verify reputation and ratings are preserved - const updatedAgentData = await registry.getAgentData(agentAddress); - expect(updatedAgentData.reputation).to.equal(80); - expect(updatedAgentData.totalRatings).to.equal(1); - expect(updatedAgentData.name).to.equal(newAgentName); - expect(updatedAgentData.agentUri).to.equal(newAgentUri); }); - it("Should allow multiple updates", async function () { - // First update - await registry.connect(agentOwner).setAgentData( - agentAddress, - "First Update", - "https://first-update.com" - ); - - let agentData = await registry.getAgentData(agentAddress); - expect(agentData.name).to.equal("First Update"); - expect(agentData.agentUri).to.equal("https://first-update.com"); - - // Second update - await registry.connect(agentOwner).setAgentData( - agentAddress, - "Second Update", - "https://second-update.com" - ); + it("Should fail to set service registry by non-owner", async function () { + await expect( + agentsRegistry.connect(addr1).setServiceRegistry(addr2.address) + ).to.be.reverted; + }); - agentData = await registry.getAgentData(agentAddress); - expect(agentData.name).to.equal("Second Update"); - expect(agentData.agentUri).to.equal("https://second-update.com"); + it("Should fail to set zero address as service registry", async function () { + await expect( + agentsRegistry.connect(owner).setServiceRegistry(ZERO_ADDRESS) + ).to.be.revertedWith("Invalid address"); }); }); - describe('#RemoveAgent', () => { + describe("7. Access Control", function () { beforeEach(async function () { - await serviceRegistry.registerService("RemoveService", "Category1", "Description1"); - await registry.connect(agentOwner).registerAgentWithService( - agentAddress, - "Service Agent", - agentUri, - "RemoveService", - ethers.parseEther("0.01"), - ethers.ZeroAddress + await agentsRegistry.connect(addr1).registerAgent( + agentAddr1.address, + AGENT_NAME_1, + AGENT_URI_1 ); }); - it("Should successfully remove an agent", async function () { - const removeTx = registry.connect(agentOwner).removeAgent(agentAddress); - - await expect(removeTx) - .to.emit(registry, "AgentRemoved") - .withArgs(agentAddress, agentOwner.address) - .to.emit(registry, "ProposalRemoved") - .withArgs(agentAddress, 1); - - // Verify agent data is cleared - const agentData = await registry.getAgentData(agentAddress); - expect(agentData.agent).to.equal(ethers.ZeroAddress); - expect(agentData.owner).to.equal(ethers.ZeroAddress); - expect(agentData.name).to.equal(""); - expect(agentData.agentUri).to.equal(""); - expect(agentData.reputation).to.equal(0); - expect(agentData.totalRatings).to.equal(0); - - // Verify proposal is removed - const proposal = await registry.getProposal(1); - expect(proposal.issuer).to.equal(ethers.ZeroAddress); - expect(proposal.serviceName).to.equal(""); - expect(proposal.isActive).to.equal(false); - }); - - it("Should not allow non-owner to remove agent", async function () { - const [, , , unauthorizedUser] = await ethers.getSigners(); - + it("Should enforce onlyAgentOwner modifier", async function () { + // Try to update agent data as non-owner await expect( - registry.connect(unauthorizedUser).removeAgent(agentAddress) + agentsRegistry.connect(addr2).setAgentData( + agentAddr1.address, + "New Name", + "New URI" + ) ).to.be.revertedWith("Not the owner of the agent"); - }); - it("Should not allow removing non-existent agent", async function () { - const [, , , , unregisteredAgent] = await ethers.getSigners(); - + // Try to remove agent as non-owner await expect( - registry.connect(unregisteredAgent).removeAgent(unregisteredAgent.address) + agentsRegistry.connect(addr2).removeAgent(agentAddr1.address) ).to.be.revertedWith("Not the owner of the agent"); }); - it("Should allow re-registration after removing", async function () { - // Remove agent - await registry.connect(agentOwner).removeAgent(agentAddress); - - // Re-register the same agent - const reregisterTx = registry.connect(agentOwner).registerAgent( - agentAddress, - "Re-registered Agent", - "https://new-uri.com" - ); - - await expect(reregisterTx) - .to.emit(registry, "AgentRegistered") - .withArgs(agentAddress, agentOwner.address, "Re-registered Agent", "https://new-uri.com"); - - const agentData = await registry.getAgentData(agentAddress); - expect(agentData.name).to.equal("Re-registered Agent"); - expect(agentData.agentUri).to.equal("https://new-uri.com"); - expect(agentData.owner).to.equal(agentOwner.address); - expect(agentData.agent).to.equal(agentAddress); - expect(agentData.reputation).to.equal(0); - expect(agentData.totalRatings).to.equal(0); + it("Should enforce onlyOwner modifier", async function () { + await expect( + agentsRegistry.connect(addr1).setServiceRegistry(addr2.address) + ).to.be.reverted; }); + }); - it("Should remove multiple proposals when removing", async function () { - // Add another proposal - await registry.connect(agentOwner).addProposal( - agentAddress, - "RemoveService", - ethers.parseEther("0.02"), - ethers.ZeroAddress - ); - - const removeTx = registry.connect(agentOwner).removeAgent(agentAddress); - - await expect(removeTx) - .to.emit(registry, "AgentRemoved") - .withArgs(agentAddress, agentOwner.address) - .to.emit(registry, "ProposalRemoved") - .withArgs(agentAddress, 2); - - // Verify that only the active proposal was removed (proposal 1 was already removed) - const proposal1 = await registry.getProposal(1); - const proposal2 = await registry.getProposal(2); - - // Proposal 1 should remain in its already-removed state - expect(proposal1.issuer).to.equal(ethers.ZeroAddress); - expect(proposal1.isActive).to.equal(false); - - // Proposal 2 should now be removed by remove - expect(proposal2.issuer).to.equal(ethers.ZeroAddress); - expect(proposal2.isActive).to.equal(false); - }); - - it("Should preserve reputation history for other agents", async function () { - // Register another agent - const [, , , , secondAgent] = await ethers.getSigners(); - await registry.connect(agentOwner).registerAgent( - secondAgent.address, - "Second Agent", - agentUri + describe("8. Data Retrieval", function () { + beforeEach(async function () { + await agentsRegistry.connect(addr1).registerAgent( + agentAddr1.address, + AGENT_NAME_1, + AGENT_URI_1 ); - - // Add reputation to both agents - await registry.connect(admin).setTaskRegistry(admin); - await registry.connect(admin).addRating(agentAddress, 80); - await registry.connect(admin).addRating(secondAgent.address, 90); - - // Remove first agent - await registry.connect(agentOwner).removeAgent(agentAddress); - - // Verify second agent's reputation is preserved - const secondAgentData = await registry.getAgentData(secondAgent.address); - expect(secondAgentData.reputation).to.equal(90); - expect(secondAgentData.totalRatings).to.equal(1); - - // Verify first agent's data is cleared - const firstAgentData = await registry.getAgentData(agentAddress); - expect(firstAgentData.reputation).to.equal(0); - expect(firstAgentData.totalRatings).to.equal(0); + await agentsRegistry.connect(addr2).addRating(agentAddr1.address, 75); }); - it("Should handle removing agent with no proposals", async function () { - // Register agent without proposals - const [, , , , agentWithoutProposals] = await ethers.getSigners(); - await registry.connect(agentOwner).registerAgent( - agentWithoutProposals.address, - "Agent Without Proposals", - agentUri - ); - - const removeTx = registry.connect(agentOwner).removeAgent(agentWithoutProposals.address); - - await expect(removeTx) - .to.emit(registry, "AgentRemoved") - .withArgs(agentWithoutProposals.address, agentOwner.address); + it("Should retrieve complete agent data", async function () { + const agentData = await agentsRegistry.getAgentData(agentAddr1.address); - // Should not emit ProposalRemoved events since there are no proposals - await expect(removeTx).to.not.emit(registry, "ProposalRemoved"); - - // Verify agent data is cleared - const agentData = await registry.getAgentData(agentWithoutProposals.address); - expect(agentData.agent).to.equal(ethers.ZeroAddress); + expect(agentData.name).to.equal(AGENT_NAME_1); + expect(agentData.agentUri).to.equal(AGENT_URI_1); + expect(agentData.owner).to.equal(addr1.address); + expect(agentData.agent).to.equal(agentAddr1.address); + expect(agentData.reputation).to.equal(75); + expect(agentData.totalRatings).to.equal(1); }); - it("Should handle removing agent after some proposals were already removed", async function () { - // Add multiple proposals - await registry.connect(agentOwner).addProposal( - agentAddress, - "RemoveService", - ethers.parseEther("0.02"), - ethers.ZeroAddress - ); - - // Remove one proposal manually - await registry.connect(agentOwner).removeProposal(agentAddress, 1); - - // Remove agent (should only remove remaining active proposal) - const removeTx = registry.connect(agentOwner).removeAgent(agentAddress); - - await expect(removeTx) - .to.emit(registry, "AgentRemoved") - .withArgs(agentAddress, agentOwner.address) - .to.emit(registry, "ProposalRemoved") - .withArgs(agentAddress, 2); + it("Should retrieve reputation separately", async function () { + const reputation = await agentsRegistry.getReputation(agentAddr1.address); + expect(reputation).to.equal(75); + }); - // Verify both proposals are now removed (one was manually removed, one removed by remove) - const proposal1 = await registry.getProposal(1); - const proposal2 = await registry.getProposal(2); + it("Should return zero values for non-existent agent", async function () { + const agentData = await agentsRegistry.getAgentData(agentAddr2.address); - expect(proposal1.issuer).to.equal(ethers.ZeroAddress); - expect(proposal1.isActive).to.equal(false); - expect(proposal2.issuer).to.equal(ethers.ZeroAddress); - expect(proposal2.isActive).to.equal(false); + expect(agentData.name).to.equal(""); + expect(agentData.agentUri).to.equal(""); + expect(agentData.owner).to.equal(ZERO_ADDRESS); + expect(agentData.agent).to.equal(ZERO_ADDRESS); + expect(agentData.reputation).to.equal(0); + expect(agentData.totalRatings).to.equal(0); }); }); -}); +}); \ No newline at end of file diff --git a/packages/contracts/test/ServiceRegistry.test.js b/packages/contracts/test/ServiceRegistry.test.js index 74339f3..a4ae748 100644 --- a/packages/contracts/test/ServiceRegistry.test.js +++ b/packages/contracts/test/ServiceRegistry.test.js @@ -1,70 +1,293 @@ const { expect } = require("chai"); const { ethers, upgrades } = require("hardhat"); -describe("ServiceRegistryUpgradeable", function () { +describe("ServiceRegistryUpgradeable V2 - Phase 1: Core Functionality", function () { let ServiceRegistry; let registry; - let owner, addr1; + let owner, addr1, addr2, agentAddr1, agentAddr2; - let service1 = "Service1"; - let category1 = "Category1"; - let description1 = "Description1"; - - let category2 = "Category2"; - let description2 = "Description2"; + // Test data constants + const IPFS_URI_1 = "ipfs://QmTest1Hash/metadata.json"; + const IPFS_URI_2 = "ipfs://QmTest2Hash/metadata.json"; + const IPFS_URI_3 = "ipfs://QmTest3Hash/metadata.json"; + const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; beforeEach(async function () { - [owner, addr1] = await ethers.getSigners(); + [owner, addr1, addr2, agentAddr1, agentAddr2] = await ethers.getSigners(); ServiceRegistry = await ethers.getContractFactory("ServiceRegistryUpgradeable"); registry = await upgrades.deployProxy(ServiceRegistry, [], { initializer: "initialize", kind: "uups" }); + await registry.waitForDeployment(); }); - it("Should register a new service", async function () { - const request = registry.registerService(service1, category1, description1) + describe("1. Contract Initialization & Deployment", function () { + it("Should initialize with correct initial state", async function () { + // Check initial state + expect(await registry.nextServiceId()).to.equal(0); + expect(await registry.totalServices()).to.equal(0); + expect(await registry.owner()).to.equal(owner.address); + }); - await expect(request) - .to.emit(registry, "ServiceRegistered") - .withArgs(service1, category1, description1); + it("Should be deployed as UUPS proxy", async function () { + // Verify proxy deployment + const implementationAddress = await upgrades.erc1967.getImplementationAddress(await registry.getAddress()); + expect(implementationAddress).to.not.equal(ZERO_ADDRESS); + }); - const isRegistered = await registry.isServiceRegistered(service1); - expect(isRegistered).to.be.true; + it("Should prevent multiple initialization", async function () { + // Try to initialize again - should revert with custom error + await expect(registry.initialize()).to.be.reverted; + }); - const service = await registry.getService(service1); - expect(service.name).to.equal(service1); - expect(service.category).to.equal(category1); - expect(service.description).to.equal(description1); + it("Should set correct owner during initialization", async function () { + expect(await registry.owner()).to.equal(owner.address); + }); }); - it("Should not register a service twice", async function () { - await registry.registerService(service1, category1, description1); - await expect(registry.registerService(service1, category1, description1)).to.be.revertedWith("Service already registered"); - }); + describe("2. Basic Service Registration", function () { + it("Should register a service with IPFS URI and agent address", async function () { + const tx = await registry.connect(addr1).registerService(IPFS_URI_1, agentAddr1.address); + + // Check event emission + await expect(tx) + .to.emit(registry, "ServiceRegistered") + .withArgs(1, addr1.address, IPFS_URI_1); + + // Verify service data + const service = await registry.getService(1); + expect(service.id).to.equal(1); + expect(service.owner).to.equal(addr1.address); + expect(service.agentAddress).to.equal(agentAddr1.address); + expect(service.serviceUri).to.equal(IPFS_URI_1); + expect(service.status).to.equal(0); // DRAFT + expect(service.version).to.equal(1); + + // Check counters + expect(await registry.nextServiceId()).to.equal(1); + expect(await registry.getTotalServiceCount()).to.equal(1); + }); + + it("Should register a service with URI only (no agent)", async function () { + const tx = await registry.connect(addr1).registerService(IPFS_URI_1, ZERO_ADDRESS); + + await expect(tx) + .to.emit(registry, "ServiceRegistered") + .withArgs(1, addr1.address, IPFS_URI_1); + + const service = await registry.getService(1); + expect(service.agentAddress).to.equal(ZERO_ADDRESS); + expect(service.serviceUri).to.equal(IPFS_URI_1); + expect(service.status).to.equal(0); // DRAFT + }); + + it("Should auto-increment service IDs correctly", async function () { + // Register first service + await registry.connect(addr1).registerService(IPFS_URI_1, agentAddr1.address); + expect(await registry.nextServiceId()).to.equal(1); + + // Register second service + await registry.connect(addr2).registerService(IPFS_URI_2, agentAddr2.address); + expect(await registry.nextServiceId()).to.equal(2); + + // Register third service + await registry.connect(addr1).registerService(IPFS_URI_3, ZERO_ADDRESS); + expect(await registry.nextServiceId()).to.equal(3); + + // Verify all services exist with correct IDs + const service1 = await registry.getService(1); + const service2 = await registry.getService(2); + const service3 = await registry.getService(3); + + expect(service1.id).to.equal(1); + expect(service2.id).to.equal(2); + expect(service3.id).to.equal(3); + + expect(await registry.getTotalServiceCount()).to.equal(3); + }); + + it("Should fail on empty service URI", async function () { + await expect(registry.connect(addr1).registerService("", agentAddr1.address)) + .to.be.revertedWith("Service URI required"); + }); + + it("Should update servicesByOwner mapping correctly", async function () { + // Register services by different owners + await registry.connect(addr1).registerService(IPFS_URI_1, agentAddr1.address); + await registry.connect(addr1).registerService(IPFS_URI_2, agentAddr2.address); + await registry.connect(addr2).registerService(IPFS_URI_3, ZERO_ADDRESS); + + // Check servicesByOwner mapping + const addr1Services = await registry.getServicesByOwner(addr1.address); + const addr2Services = await registry.getServicesByOwner(addr2.address); + + expect(addr1Services.length).to.equal(2); + expect(addr1Services[0]).to.equal(1); + expect(addr1Services[1]).to.equal(2); + + expect(addr2Services.length).to.equal(1); + expect(addr2Services[0]).to.equal(3); + }); + + it("Should update servicesByAgent mapping when agent is provided", async function () { + // Register services with agents + await registry.connect(addr1).registerService(IPFS_URI_1, agentAddr1.address); + await registry.connect(addr2).registerService(IPFS_URI_2, agentAddr1.address); // Same agent + await registry.connect(addr1).registerService(IPFS_URI_3, ZERO_ADDRESS); // No agent + + // Check servicesByAgent mapping + const agent1Services = await registry.getServicesByAgent(agentAddr1.address); + const agent2Services = await registry.getServicesByAgent(agentAddr2.address); + const zeroServices = await registry.getServicesByAgent(ZERO_ADDRESS); + + expect(agent1Services.length).to.equal(2); + expect(agent1Services[0]).to.equal(1); + expect(agent1Services[1]).to.equal(2); - it("Should not register a service with an empty name", async function () { - await expect(registry.registerService("", category1, description1)).to.be.revertedWith("Invalid service name"); + expect(agent2Services.length).to.equal(0); + expect(zeroServices.length).to.equal(0); // Zero address shouldn't have services + }); + + it("Should emit ServiceRegistered event with correct parameters", async function () { + const tx = registry.connect(addr1).registerService(IPFS_URI_1, agentAddr1.address); + + await expect(tx) + .to.emit(registry, "ServiceRegistered") + .withArgs(1, addr1.address, IPFS_URI_1); + }); }); - it("Should not update a service if it is not registered", async function () { - await expect(registry.updateService(service1, category2, description2)).to.be.revertedWith("Service not registered"); + describe("3. Service Retrieval & Basic Queries", function () { + beforeEach(async function () { + // Setup test data + await registry.connect(addr1).registerService(IPFS_URI_1, agentAddr1.address); + await registry.connect(addr1).registerService(IPFS_URI_2, agentAddr2.address); + await registry.connect(addr2).registerService(IPFS_URI_3, ZERO_ADDRESS); + }); + + it("Should retrieve service by ID correctly", async function () { + const service = await registry.getService(1); + + expect(service.id).to.equal(1); + expect(service.owner).to.equal(addr1.address); + expect(service.agentAddress).to.equal(agentAddr1.address); + expect(service.serviceUri).to.equal(IPFS_URI_1); + expect(service.status).to.equal(0); // DRAFT + expect(service.version).to.equal(1); + }); + + it("Should fail to retrieve non-existent service", async function () { + await expect(registry.getService(999)) + .to.be.revertedWith("Service does not exist"); + }); + + it("Should fail to retrieve service with ID 0", async function () { + await expect(registry.getService(0)) + .to.be.revertedWith("Service does not exist"); + }); + + it("Should get services by owner correctly", async function () { + const addr1Services = await registry.getServicesByOwner(addr1.address); + const addr2Services = await registry.getServicesByOwner(addr2.address); + + expect(addr1Services.length).to.equal(2); + expect(addr1Services).to.deep.equal([ethers.getBigInt(1), ethers.getBigInt(2)]); + + expect(addr2Services.length).to.equal(1); + expect(addr2Services).to.deep.equal([ethers.getBigInt(3)]); + }); + + it("Should return empty array for owner with no services", async function () { + const [newAddr] = await ethers.getSigners(); + const services = await registry.getServicesByOwner(newAddr.address); + expect(services.length).to.equal(0); + }); + + it("Should get services by agent correctly", async function () { + const agent1Services = await registry.getServicesByAgent(agentAddr1.address); + const agent2Services = await registry.getServicesByAgent(agentAddr2.address); + + expect(agent1Services.length).to.equal(1); + expect(agent1Services).to.deep.equal([ethers.getBigInt(1)]); + + expect(agent2Services.length).to.equal(1); + expect(agent2Services).to.deep.equal([ethers.getBigInt(2)]); + }); + + it("Should return total service count correctly", async function () { + expect(await registry.getTotalServiceCount()).to.equal(3); + + // Register another service + await registry.connect(addr2).registerService(IPFS_URI_1, agentAddr1.address); + expect(await registry.getTotalServiceCount()).to.equal(4); + }); + + it("Should get service owner correctly", async function () { + expect(await registry.getServiceOwner(1)).to.equal(addr1.address); + expect(await registry.getServiceOwner(2)).to.equal(addr1.address); + expect(await registry.getServiceOwner(3)).to.equal(addr2.address); + }); + + it("Should fail to get owner of non-existent service", async function () { + await expect(registry.getServiceOwner(999)) + .to.be.revertedWith("Service does not exist"); + }); }); + describe("4. Service Existence Validation", function () { + beforeEach(async function () { + await registry.connect(addr1).registerService(IPFS_URI_1, agentAddr1.address); + }); - it("Should update a service", async function () { - await registry.registerService(service1, category1, description1); - const request = registry.updateService(service1, category2, description2); + it("Should validate existing service ID", async function () { + // This should not revert + await registry.getService(1); + }); - await expect(request) - .to.emit(registry, "ServiceUpdated") - .withArgs(service1, category2, description2); + it("Should reject service ID 0", async function () { + await expect(registry.getService(0)) + .to.be.revertedWith("Service does not exist"); + }); - const service = await registry.getService(service1); - expect(service.name).to.equal(service1); - expect(service.category).to.equal(category2); - expect(service.description).to.equal(description2); + it("Should reject service ID greater than nextServiceId", async function () { + await expect(registry.getService(2)) + .to.be.revertedWith("Service does not exist"); + }); + it("Should handle edge case at boundary", async function () { + // Register second service + await registry.connect(addr1).registerService(IPFS_URI_2, agentAddr2.address); + + // Service ID 2 should exist + await registry.getService(2); + + // Service ID 3 should not exist + await expect(registry.getService(3)) + .to.be.revertedWith("Service does not exist"); + }); }); -}) \ No newline at end of file + describe("5. Gas Optimization Verification", function () { + it("Should register service efficiently", async function () { + const tx = await registry.connect(addr1).registerService(IPFS_URI_1, agentAddr1.address); + const receipt = await tx.wait(); + + // Gas usage should be reasonable (adjust threshold as needed) + expect(receipt.gasUsed).to.be.below(350000); // 350k gas threshold for initial service + }); + + it("Should handle multiple services per owner efficiently", async function () { + // Register multiple services and check gas doesn't increase significantly + const tx1 = await registry.connect(addr1).registerService(IPFS_URI_1, agentAddr1.address); + const receipt1 = await tx1.wait(); + + const tx2 = await registry.connect(addr1).registerService(IPFS_URI_2, agentAddr2.address); + const receipt2 = await tx2.wait(); + + // Second registration should not be significantly more expensive + const gasIncrease = receipt2.gasUsed - receipt1.gasUsed; + expect(gasIncrease).to.be.below(50000); // 50k gas increase threshold + }); + }); +}); \ No newline at end of file diff --git a/packages/contracts/test/ServiceRegistryV2.Phase2.test.js b/packages/contracts/test/ServiceRegistryV2.Phase2.test.js new file mode 100644 index 0000000..f43aebf --- /dev/null +++ b/packages/contracts/test/ServiceRegistryV2.Phase2.test.js @@ -0,0 +1,430 @@ +const { expect } = require("chai"); +const { ethers, upgrades } = require("hardhat"); + +describe("ServiceRegistryUpgradeable V2 - Phase 2: Advanced Features & Agent Management", function () { + let ServiceRegistry; + let registry; + let owner, addr1, addr2, addr3, agentAddr1, agentAddr2; + + // Test data constants + const IPFS_URI_1 = "ipfs://QmTest1Hash/metadata.json"; + const IPFS_URI_2 = "ipfs://QmTest2Hash/metadata.json"; + const IPFS_URI_UPDATED = "ipfs://QmUpdatedHash/metadata.json"; + const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; + + // Service status enum values + const ServiceStatus = { + DRAFT: 0, + PUBLISHED: 1, + ARCHIVED: 2, + DELETED: 3 + }; + + beforeEach(async function () { + [owner, addr1, addr2, addr3, agentAddr1, agentAddr2] = await ethers.getSigners(); + ServiceRegistry = await ethers.getContractFactory("ServiceRegistryUpgradeable"); + registry = await upgrades.deployProxy(ServiceRegistry, [], { + initializer: "initialize", + kind: "uups" + }); + await registry.waitForDeployment(); + }); + + describe("1. Service Status Management", function () { + let serviceId; + + beforeEach(async function () { + // Register a service with an agent + await registry.connect(addr1).registerService(IPFS_URI_1, agentAddr1.address); + serviceId = 1; + }); + + it("Should change status from DRAFT to PUBLISHED with agent assigned", async function () { + const tx = await registry.connect(addr1).setServiceStatus(serviceId, ServiceStatus.PUBLISHED); + + await expect(tx) + .to.emit(registry, "ServiceStatusChanged") + .withArgs(serviceId, ServiceStatus.DRAFT, ServiceStatus.PUBLISHED); + + const service = await registry.getService(serviceId); + expect(service.status).to.equal(ServiceStatus.PUBLISHED); + }); + + it("Should fail to publish service without agent", async function () { + // Register service without agent + await registry.connect(addr1).registerService(IPFS_URI_2, ZERO_ADDRESS); + const serviceIdNoAgent = 2; + + await expect( + registry.connect(addr1).setServiceStatus(serviceIdNoAgent, ServiceStatus.PUBLISHED) + ).to.be.revertedWith("Service must have agent to be published"); + }); + + it("Should change status from PUBLISHED to ARCHIVED", async function () { + // First publish the service + await registry.connect(addr1).setServiceStatus(serviceId, ServiceStatus.PUBLISHED); + + // Then archive it + const tx = await registry.connect(addr1).setServiceStatus(serviceId, ServiceStatus.ARCHIVED); + + await expect(tx) + .to.emit(registry, "ServiceStatusChanged") + .withArgs(serviceId, ServiceStatus.PUBLISHED, ServiceStatus.ARCHIVED); + + const service = await registry.getService(serviceId); + expect(service.status).to.equal(ServiceStatus.ARCHIVED); + }); + + it("Should change status from PUBLISHED to DRAFT", async function () { + // First publish the service + await registry.connect(addr1).setServiceStatus(serviceId, ServiceStatus.PUBLISHED); + + // Then set back to draft + const tx = await registry.connect(addr1).setServiceStatus(serviceId, ServiceStatus.DRAFT); + + await expect(tx) + .to.emit(registry, "ServiceStatusChanged") + .withArgs(serviceId, ServiceStatus.PUBLISHED, ServiceStatus.DRAFT); + + const service = await registry.getService(serviceId); + expect(service.status).to.equal(ServiceStatus.DRAFT); + }); + + it("Should prevent invalid status transitions", async function () { + // Try to go from DRAFT to DELETED (should be invalid) + await expect( + registry.connect(addr1).setServiceStatus(serviceId, ServiceStatus.DELETED) + ).to.be.revertedWith("Invalid status transition"); + }); + + it("Should fail to change status by non-owner", async function () { + await expect( + registry.connect(addr2).setServiceStatus(serviceId, ServiceStatus.PUBLISHED) + ).to.be.revertedWith("Not service owner"); + }); + + it("Should increment version on status change", async function () { + const serviceBefore = await registry.getService(serviceId); + const versionBefore = serviceBefore.version; + + await registry.connect(addr1).setServiceStatus(serviceId, ServiceStatus.PUBLISHED); + + const serviceAfter = await registry.getService(serviceId); + expect(serviceAfter.version).to.equal(versionBefore + 1n); + }); + }); + + describe("2. Agent Assignment & Management", function () { + let serviceId; + + beforeEach(async function () { + // Register a service without an agent initially + await registry.connect(addr1).registerService(IPFS_URI_1, ZERO_ADDRESS); + serviceId = 1; + }); + + it("Should assign agent to service", async function () { + const tx = await registry.connect(addr1).assignAgentToService(serviceId, agentAddr1.address); + + await expect(tx) + .to.emit(registry, "ServiceAgentAssigned") + .withArgs(serviceId, agentAddr1.address); + + const service = await registry.getService(serviceId); + expect(service.agentAddress).to.equal(agentAddr1.address); + }); + + it("Should update servicesByAgent mapping on assignment", async function () { + await registry.connect(addr1).assignAgentToService(serviceId, agentAddr1.address); + + const agentServices = await registry.getServicesByAgent(agentAddr1.address); + expect(agentServices.length).to.equal(1); + expect(agentServices[0]).to.equal(serviceId); + }); + + it("Should reassign agent and clean up old mapping", async function () { + // Assign first agent + await registry.connect(addr1).assignAgentToService(serviceId, agentAddr1.address); + + // Reassign to second agent + const tx = await registry.connect(addr1).assignAgentToService(serviceId, agentAddr2.address); + + await expect(tx) + .to.emit(registry, "ServiceAgentUnassigned") + .withArgs(serviceId, agentAddr1.address); + + await expect(tx) + .to.emit(registry, "ServiceAgentAssigned") + .withArgs(serviceId, agentAddr2.address); + + // Check mappings + const agent1Services = await registry.getServicesByAgent(agentAddr1.address); + const agent2Services = await registry.getServicesByAgent(agentAddr2.address); + + expect(agent1Services.length).to.equal(0); + expect(agent2Services.length).to.equal(1); + expect(agent2Services[0]).to.equal(serviceId); + }); + + it("Should fail to assign zero address as agent", async function () { + await expect( + registry.connect(addr1).assignAgentToService(serviceId, ZERO_ADDRESS) + ).to.be.revertedWith("Invalid agent address"); + }); + + it("Should fail to assign agent by non-owner", async function () { + await expect( + registry.connect(addr2).assignAgentToService(serviceId, agentAddr1.address) + ).to.be.revertedWith("Not service owner"); + }); + + it("Should handle multiple services per agent", async function () { + // Register second service + await registry.connect(addr1).registerService(IPFS_URI_2, ZERO_ADDRESS); + const serviceId2 = 2; + + // Assign same agent to both services + await registry.connect(addr1).assignAgentToService(serviceId, agentAddr1.address); + await registry.connect(addr1).assignAgentToService(serviceId2, agentAddr1.address); + + const agentServices = await registry.getServicesByAgent(agentAddr1.address); + expect(agentServices.length).to.equal(2); + expect(agentServices).to.deep.equal([ethers.getBigInt(1), ethers.getBigInt(2)]); + }); + }); + + describe("3. Agent Unassignment with Status", function () { + let serviceId; + + beforeEach(async function () { + // Register a service with an agent and publish it + await registry.connect(addr1).registerService(IPFS_URI_1, agentAddr1.address); + serviceId = 1; + await registry.connect(addr1).setServiceStatus(serviceId, ServiceStatus.PUBLISHED); + }); + + it("Should unassign agent and set status to DRAFT", async function () { + const tx = await registry.connect(addr1).unassignAgentFromService(serviceId, ServiceStatus.DRAFT); + + await expect(tx) + .to.emit(registry, "ServiceAgentUnassigned") + .withArgs(serviceId, agentAddr1.address); + + await expect(tx) + .to.emit(registry, "ServiceStatusChanged") + .withArgs(serviceId, ServiceStatus.PUBLISHED, ServiceStatus.DRAFT); + + const service = await registry.getService(serviceId); + expect(service.agentAddress).to.equal(ZERO_ADDRESS); + expect(service.status).to.equal(ServiceStatus.DRAFT); + }); + + it("Should unassign agent and set status to ARCHIVED", async function () { + const tx = await registry.connect(addr1).unassignAgentFromService(serviceId, ServiceStatus.ARCHIVED); + + await expect(tx) + .to.emit(registry, "ServiceAgentUnassigned") + .withArgs(serviceId, agentAddr1.address); + + await expect(tx) + .to.emit(registry, "ServiceStatusChanged") + .withArgs(serviceId, ServiceStatus.PUBLISHED, ServiceStatus.ARCHIVED); + + const service = await registry.getService(serviceId); + expect(service.agentAddress).to.equal(ZERO_ADDRESS); + expect(service.status).to.equal(ServiceStatus.ARCHIVED); + }); + + it("Should fail with invalid status (PUBLISHED)", async function () { + await expect( + registry.connect(addr1).unassignAgentFromService(serviceId, ServiceStatus.PUBLISHED) + ).to.be.revertedWith("Status must be DRAFT or ARCHIVED"); + }); + + it("Should fail with invalid status (DELETED)", async function () { + await expect( + registry.connect(addr1).unassignAgentFromService(serviceId, ServiceStatus.DELETED) + ).to.be.revertedWith("Status must be DRAFT or ARCHIVED"); + }); + + it("Should fail when no agent is assigned", async function () { + // Create service without agent + await registry.connect(addr1).registerService(IPFS_URI_2, ZERO_ADDRESS); + const serviceId2 = 2; + + await expect( + registry.connect(addr1).unassignAgentFromService(serviceId2, ServiceStatus.DRAFT) + ).to.be.revertedWith("No agent assigned"); + }); + + it("Should increment version on unassignment", async function () { + const serviceBefore = await registry.getService(serviceId); + const versionBefore = serviceBefore.version; + + await registry.connect(addr1).unassignAgentFromService(serviceId, ServiceStatus.DRAFT); + + const serviceAfter = await registry.getService(serviceId); + expect(serviceAfter.version).to.equal(versionBefore + 1n); + }); + + it("Should properly clean up servicesByAgent mapping", async function () { + await registry.connect(addr1).unassignAgentFromService(serviceId, ServiceStatus.DRAFT); + + const agentServices = await registry.getServicesByAgent(agentAddr1.address); + expect(agentServices.length).to.equal(0); + }); + }); + + describe("4. Service Updates", function () { + let serviceId; + + beforeEach(async function () { + await registry.connect(addr1).registerService(IPFS_URI_1, agentAddr1.address); + serviceId = 1; + }); + + it("Should update service URI", async function () { + const tx = await registry.connect(addr1).updateService(serviceId, IPFS_URI_UPDATED); + + await expect(tx) + .to.emit(registry, "ServiceUpdated") + .withArgs(serviceId, IPFS_URI_UPDATED, 2); // version 2 after update + + const service = await registry.getService(serviceId); + expect(service.serviceUri).to.equal(IPFS_URI_UPDATED); + }); + + it("Should increment version on update", async function () { + const serviceBefore = await registry.getService(serviceId); + const versionBefore = serviceBefore.version; + + await registry.connect(addr1).updateService(serviceId, IPFS_URI_UPDATED); + + const serviceAfter = await registry.getService(serviceId); + expect(serviceAfter.version).to.equal(versionBefore + 1n); + }); + + it("Should fail to update with empty URI", async function () { + await expect( + registry.connect(addr1).updateService(serviceId, "") + ).to.be.revertedWith("Service URI required"); + }); + + it("Should fail to update by non-owner", async function () { + await expect( + registry.connect(addr2).updateService(serviceId, IPFS_URI_UPDATED) + ).to.be.revertedWith("Not service owner"); + }); + + it("Should fail to update non-existent service", async function () { + await expect( + registry.connect(addr1).updateService(999, IPFS_URI_UPDATED) + ).to.be.revertedWith("Service does not exist"); + }); + + it("Should allow multiple updates", async function () { + const uri1 = "ipfs://Update1/metadata.json"; + const uri2 = "ipfs://Update2/metadata.json"; + const uri3 = "ipfs://Update3/metadata.json"; + + await registry.connect(addr1).updateService(serviceId, uri1); + await registry.connect(addr1).updateService(serviceId, uri2); + await registry.connect(addr1).updateService(serviceId, uri3); + + const service = await registry.getService(serviceId); + expect(service.serviceUri).to.equal(uri3); + expect(service.version).to.equal(4); // Initial version 1 + 3 updates + }); + + it("Should not affect other service properties during update", async function () { + const serviceBefore = await registry.getService(serviceId); + + await registry.connect(addr1).updateService(serviceId, IPFS_URI_UPDATED); + + const serviceAfter = await registry.getService(serviceId); + expect(serviceAfter.id).to.equal(serviceBefore.id); + expect(serviceAfter.owner).to.equal(serviceBefore.owner); + expect(serviceAfter.agentAddress).to.equal(serviceBefore.agentAddress); + expect(serviceAfter.status).to.equal(serviceBefore.status); + }); + }); + + describe("5. Complex Workflow Scenarios", function () { + it("Should handle complete service lifecycle", async function () { + // 1. Register service without agent + await registry.connect(addr1).registerService(IPFS_URI_1, ZERO_ADDRESS); + const serviceId = 1; + + let service = await registry.getService(serviceId); + expect(service.status).to.equal(ServiceStatus.DRAFT); + expect(service.version).to.equal(1); + + // 2. Assign an agent + await registry.connect(addr1).assignAgentToService(serviceId, agentAddr1.address); + + // 3. Publish the service + await registry.connect(addr1).setServiceStatus(serviceId, ServiceStatus.PUBLISHED); + + service = await registry.getService(serviceId); + expect(service.status).to.equal(ServiceStatus.PUBLISHED); + + // 4. Update service metadata + await registry.connect(addr1).updateService(serviceId, IPFS_URI_UPDATED); + + service = await registry.getService(serviceId); + expect(service.version).to.equal(3); // version 1 + setServiceStatus + updateService + + // 5. Archive the service + await registry.connect(addr1).setServiceStatus(serviceId, ServiceStatus.ARCHIVED); + + service = await registry.getService(serviceId); + expect(service.status).to.equal(ServiceStatus.ARCHIVED); + expect(service.version).to.equal(4); // version 3 + setServiceStatus + }); + + it("Should handle agent reassignment workflow", async function () { + // Register service with first agent + await registry.connect(addr1).registerService(IPFS_URI_1, agentAddr1.address); + const serviceId = 1; + + // Publish with first agent + await registry.connect(addr1).setServiceStatus(serviceId, ServiceStatus.PUBLISHED); + + // Unassign and set to draft + await registry.connect(addr1).unassignAgentFromService(serviceId, ServiceStatus.DRAFT); + + // Assign new agent + await registry.connect(addr1).assignAgentToService(serviceId, agentAddr2.address); + + // Republish with new agent + await registry.connect(addr1).setServiceStatus(serviceId, ServiceStatus.PUBLISHED); + + const service = await registry.getService(serviceId); + expect(service.agentAddress).to.equal(agentAddr2.address); + expect(service.status).to.equal(ServiceStatus.PUBLISHED); + }); + + it("Should maintain consistency across multiple services and agents", async function () { + // Create multiple services with different configurations + await registry.connect(addr1).registerService(IPFS_URI_1, agentAddr1.address); + await registry.connect(addr2).registerService(IPFS_URI_2, agentAddr1.address); + await registry.connect(addr1).registerService("ipfs://Service3", agentAddr2.address); + + // Verify agent1 has 2 services + let agent1Services = await registry.getServicesByAgent(agentAddr1.address); + expect(agent1Services.length).to.equal(2); + + // Reassign service 1 from agent1 to agent2 + await registry.connect(addr1).assignAgentToService(1, agentAddr2.address); + + // Verify updated mappings + agent1Services = await registry.getServicesByAgent(agentAddr1.address); + const agent2Services = await registry.getServicesByAgent(agentAddr2.address); + + expect(agent1Services.length).to.equal(1); + expect(agent1Services[0]).to.equal(2); + expect(agent2Services.length).to.equal(2); + expect(agent2Services).to.deep.equal([ethers.getBigInt(3), ethers.getBigInt(1)]); + }); + }); +}); \ No newline at end of file diff --git a/packages/contracts/test/TaskRegistry.test.js b/packages/contracts/test/TaskRegistry.test.js deleted file mode 100644 index 1a42ef1..0000000 --- a/packages/contracts/test/TaskRegistry.test.js +++ /dev/null @@ -1,473 +0,0 @@ -const { expect } = require("chai"); -const { ethers, upgrades } = require("hardhat"); -const AgentRegistryV1Artifact = require('./artifacts/AgentsRegistryV1.json') - -describe("TaskRegistryUpgradeable", function () { - let TaskRegistry, taskRegistry, AgentsRegistry, agentsRegistry; - let agentOwner, agentAddress, taskIssuer, eveAddress; - let taskPrice = ethers.parseEther("0.01"); - let proposalId = 1; - let prompt = "Test prompt"; - - async function setupWithEthProposal() { - await serviceRegistry.registerService("Service1", "Category1", "Description1"); - await agentsRegistry.connect(agentOwner).registerAgentWithService( - agentAddress, - "Service Agent", - "https://uri", - "Service1", - taskPrice, - ethers.ZeroAddress - ); - - await agentsRegistry.setTaskRegistry(taskRegistry); - } - - async function setupWithErc20Proposal() { - // Deploy mock ERC20 token - const MockERC20 = await ethers.getContractFactory("MockERC20"); - mockToken = await MockERC20.deploy("MockToken", "MTK", ethers.parseEther("1000"), 18); - - // Transfer tokens to task issuer - await mockToken.transfer(taskIssuer.address, ethers.parseEther("10")); - - // Register service - await serviceRegistry.registerService("Service1", "Category1", "Description1"); - - // Register agent with ERC20 proposal - await agentsRegistry.connect(agentOwner).registerAgentWithService( - agentAddress, - "Service Agent", - "https://uri", - "Service1", - taskPrice, - mockToken.target - ); - - await agentsRegistry.setTaskRegistry(taskRegistry); - - // Approve task registry to spend tokens - await mockToken.connect(taskIssuer).approve(taskRegistry.target, taskPrice); - } - - - beforeEach(async function () { - [taskIssuer, agentOwner, agentAddress, eveAddress] = await ethers.getSigners(); - - // Deploy ServiceRegistryUpgradeable - const ServiceRegistry = await ethers.getContractFactory("ServiceRegistryUpgradeable"); - serviceRegistry = await upgrades.deployProxy(ServiceRegistry, [], { - initializer: "initialize", - kind: "uups" - }); - - // Deploy legacy ServiceRegistry for V1 compatibility - const ServiceRegistryV1 = await ethers.getContractFactory("ServiceRegistryUpgradeable"); - const serviceRegistryV1 = await upgrades.deployProxy(ServiceRegistryV1, [], { - initializer: "initialize", - kind: "uups" - }); - - const AgentRegistryV1 = await ethers.getContractFactoryFromArtifact(AgentRegistryV1Artifact); - const agentRegistryV1 = await AgentRegistryV1.deploy(serviceRegistryV1.target); - - // Deploy AgentsRegistryUpgradeable - AgentsRegistry = await ethers.getContractFactory("AgentsRegistryUpgradeable"); - agentsRegistry = await upgrades.deployProxy(AgentsRegistry, [agentRegistryV1.target, serviceRegistry.target], { - initializer: "initialize", - kind: "uups" - }); - - // Deploy TaskRegistryUpgradeable - TaskRegistry = await ethers.getContractFactory("TaskRegistryUpgradeable"); - taskRegistry = await upgrades.deployProxy(TaskRegistry, [1, agentsRegistry.target], { - initializer: "initialize", - kind: "uups" - }); - }); - - describe("createTask", function () { - - describe("ETH proposal", function () { - beforeEach(async function () { - await setupWithEthProposal(); - }) - - it("should create a new task", async function () { - await expect(taskRegistry.createTask(prompt, proposalId, { value: taskPrice })) - .to.emit(taskRegistry, "TaskCreated") - .withArgs(taskIssuer, agentAddress, 1, proposalId, prompt); - - const task = await taskRegistry.getTask(1); - expect(task.prompt).to.equal(prompt); - expect(task.issuer).to.equal(taskIssuer); - expect(task.status).to.equal(1); // TaskStatus.ASSIGNED - expect(task.proposalId).to.equal(proposalId); - }); - - it("should fail if the price is incorrect", async function () { - - const wrongTaskPrice = ethers.parseEther("0.02"); - - await expect(taskRegistry.createTask(prompt, proposalId, { value: wrongTaskPrice })) - .to.be.revertedWith("Invalid price"); - }); - }) - - describe("ERC20 proposal", function () { - - beforeEach(async function () { - await setupWithErc20Proposal(); - }) - - it("should create a new task with ERC20 payment", async function () { - // Approve the task registry to spend tokens - await mockToken.connect(taskIssuer).approve(taskRegistry.target, taskPrice); - - await expect(taskRegistry.createTask(prompt, proposalId)) - .to.emit(taskRegistry, "TaskCreated") - .withArgs(taskIssuer, agentAddress, 1, proposalId, prompt); - - const task = await taskRegistry.getTask(1); - expect(task.prompt).to.equal(prompt); - expect(task.issuer).to.equal(taskIssuer); - expect(task.status).to.equal(1); // TaskStatus.ASSIGNED - expect(task.proposalId).to.equal(proposalId); - }); - - it("should fail if insufficient token allowance", async function () { - // Don't approve tokens or approve insufficient amount - await mockToken.connect(taskIssuer).approve(taskRegistry.target, ethers.parseEther("0.005")); - - await expect(taskRegistry.createTask(prompt, proposalId)) - .to.be.reverted; - }); - - it("should fail if insufficient token balance", async function () { - // Transfer away tokens so balance is insufficient - const balance = await mockToken.balanceOf(taskIssuer.address); - await mockToken.connect(taskIssuer).transfer(eveAddress.address, balance); - - await mockToken.connect(taskIssuer).approve(taskRegistry.target, taskPrice); - - await expect(taskRegistry.createTask(prompt, proposalId)) - .to.be.reverted; - }); - - it("should transfer tokens from issuer to contract", async function () { - const initialIssuerBalance = await mockToken.balanceOf(taskIssuer.address); - const initialContractBalance = await mockToken.balanceOf(taskRegistry.target); - - await mockToken.connect(taskIssuer).approve(taskRegistry.target, taskPrice); - await taskRegistry.createTask(prompt, proposalId); - - const finalIssuerBalance = await mockToken.balanceOf(taskIssuer.address); - const finalContractBalance = await mockToken.balanceOf(taskRegistry.target); - - expect(finalIssuerBalance).to.equal(initialIssuerBalance - taskPrice); - expect(finalContractBalance).to.equal(initialContractBalance + taskPrice); - }); - }) - }); - - describe("completeTask", function () { - describe("ETH tasks", function () { - beforeEach(async function () { - await setupWithEthProposal(); - }) - - it("should complete a task", async function () { - await expect(taskRegistry.createTask(prompt, proposalId, { value: taskPrice })) - .to.emit(taskRegistry, "TaskCreated") - .withArgs(taskIssuer, agentAddress, 1, proposalId, prompt); - - await expect(taskRegistry.connect(agentAddress).completeTask(1, "Test result")) - .to.emit(taskRegistry, "TaskStatusChanged") - .withArgs(1, 2) // TaskStatus.COMPLETED - .and.to.emit(taskRegistry, "TaskCompleted") - .withArgs(1, "Test result"); - - const updatedTask = await taskRegistry.getTask(1); - expect(updatedTask.status).to.equal(2); // TaskStatus.COMPLETED - }); - - it("should fail if not authorized", async function () { - await expect(taskRegistry.createTask(prompt, proposalId, { value: taskPrice })) - .to.emit(taskRegistry, "TaskCreated") - .withArgs(taskIssuer, agentAddress, 1, proposalId, prompt); - - await expect(taskRegistry.connect(eveAddress).completeTask(1, "Test result")) - .to.be.revertedWith("Not authorized"); - }); - - it("Cannot complete task multiple times", async function () { - await expect(taskRegistry.createTask(prompt, proposalId, { value: taskPrice })) - .to.emit(taskRegistry, "TaskCreated") - .withArgs(taskIssuer, agentAddress, 1, proposalId, prompt); - - await expect(taskRegistry.connect(agentAddress).completeTask(1, "Test result")) - - await expect(taskRegistry.connect(agentAddress).completeTask(1, "Test result")) - .to.be.revertedWith("Invalid task status"); - }); - - it("should transfer ETH back to issuer on completion", async function () { - const initialIssuerBalance = await ethers.provider.getBalance(taskIssuer.address); - - await taskRegistry.createTask(prompt, proposalId, { value: taskPrice }); - - const balanceAfterCreate = await ethers.provider.getBalance(taskIssuer.address); - expect(balanceAfterCreate).to.be.lessThan(initialIssuerBalance); - - await taskRegistry.connect(agentAddress).completeTask(1, "Test result"); - - const finalIssuerBalance = await ethers.provider.getBalance(taskIssuer.address); - expect(finalIssuerBalance).to.equal(balanceAfterCreate + taskPrice); - }); - }); - - describe("ERC20 tasks", function () { - beforeEach(async function () { - await setupWithErc20Proposal(); - }) - - it("should complete an ERC20 task", async function () { - await mockToken.connect(taskIssuer).approve(taskRegistry.target, taskPrice); - - await expect(taskRegistry.createTask(prompt, proposalId)) - .to.emit(taskRegistry, "TaskCreated") - .withArgs(taskIssuer, agentAddress, 1, proposalId, prompt); - - await expect(taskRegistry.connect(agentAddress).completeTask(1, "Test result")) - .to.emit(taskRegistry, "TaskStatusChanged") - .withArgs(1, 2) // TaskStatus.COMPLETED - .and.to.emit(taskRegistry, "TaskCompleted") - .withArgs(1, "Test result"); - - const updatedTask = await taskRegistry.getTask(1); - expect(updatedTask.status).to.equal(2); // TaskStatus.COMPLETED - expect(updatedTask.result).to.equal("Test result"); - }); - - it("should fail if not authorized for ERC20 task", async function () { - await mockToken.connect(taskIssuer).approve(taskRegistry.target, taskPrice); - - await taskRegistry.createTask(prompt, proposalId); - - await expect(taskRegistry.connect(eveAddress).completeTask(1, "Test result")) - .to.be.revertedWith("Not authorized"); - }); - - it("Cannot complete ERC20 task multiple times", async function () { - await mockToken.connect(taskIssuer).approve(taskRegistry.target, taskPrice); - - await taskRegistry.createTask(prompt, proposalId); - - await taskRegistry.connect(agentAddress).completeTask(1, "Test result"); - - await expect(taskRegistry.connect(agentAddress).completeTask(1, "Test result")) - .to.be.revertedWith("Invalid task status"); - }); - - it("should transfer tokens back to issuer on completion", async function () { - const initialIssuerBalance = await mockToken.balanceOf(taskIssuer.address); - const initialContractBalance = await mockToken.balanceOf(taskRegistry.target); - - await mockToken.connect(taskIssuer).approve(taskRegistry.target, taskPrice); - await taskRegistry.createTask(prompt, proposalId); - - const balanceAfterCreate = await mockToken.balanceOf(taskIssuer.address); - const contractBalanceAfterCreate = await mockToken.balanceOf(taskRegistry.target); - - expect(balanceAfterCreate).to.equal(initialIssuerBalance - taskPrice); - expect(contractBalanceAfterCreate).to.equal(initialContractBalance + taskPrice); - - await taskRegistry.connect(agentAddress).completeTask(1, "Test result"); - - const finalIssuerBalance = await mockToken.balanceOf(taskIssuer.address); - const finalContractBalance = await mockToken.balanceOf(taskRegistry.target); - - expect(finalIssuerBalance).to.equal(initialIssuerBalance); - expect(finalContractBalance).to.equal(initialContractBalance); - }); - - it("should handle completion with empty result", async function () { - await mockToken.connect(taskIssuer).approve(taskRegistry.target, taskPrice); - - await taskRegistry.createTask(prompt, proposalId); - - await expect(taskRegistry.connect(agentAddress).completeTask(1, "")) - .to.emit(taskRegistry, "TaskCompleted") - .withArgs(1, ""); - - const updatedTask = await taskRegistry.getTask(1); - expect(updatedTask.result).to.equal(""); - expect(updatedTask.status).to.equal(2); // TaskStatus.COMPLETED - }); - - it("should handle completion with long result", async function () { - await mockToken.connect(taskIssuer).approve(taskRegistry.target, taskPrice); - - await taskRegistry.createTask(prompt, proposalId); - - const longResult = "This is a very long result that contains multiple sentences and detailed information about the task completion. ".repeat(10); - - await expect(taskRegistry.connect(agentAddress).completeTask(1, longResult)) - .to.emit(taskRegistry, "TaskCompleted") - .withArgs(1, longResult); - - const updatedTask = await taskRegistry.getTask(1); - expect(updatedTask.result).to.equal(longResult); - }); - }); - }); - - describe("rateTask", function () { - beforeEach(async function () { - await setupWithEthProposal(); - - await expect(taskRegistry.createTask(prompt, proposalId, { value: taskPrice })).not.to.be.reverted - await expect(taskRegistry.connect(agentAddress).completeTask(1, "Test result")) - .and.to.emit(taskRegistry, "TaskCompleted") - .withArgs(1, "Test result"); - }); - - it("should rate a task", async function () { - const rating = 80; // Example rating - await expect(taskRegistry.connect(taskIssuer).rateTask(1, rating)) - .to.emit(taskRegistry, "TaskRated") - .withArgs(1, rating); - - const updatedTask = await taskRegistry.getTask(1); - expect(updatedTask.rating).to.equal(rating); - }); - - it("should fail if not the issuer", async function () { - await expect(taskRegistry.connect(eveAddress).rateTask(1, 80)) - .to.be.revertedWith("Not the issuer of the task"); - }); - - it("should fail if task is not completed", async function () { - await expect(taskRegistry.createTask(prompt, proposalId, { value: taskPrice })).not.to.be.reverted - - await expect(taskRegistry.connect(taskIssuer).rateTask(2, 80)) - .to.be.revertedWith("Task is not completed"); - }); - - it("should fail if rating is out of range", async function () { - await expect(taskRegistry.connect(taskIssuer).rateTask(1, 101)) - .to.be.revertedWith("Rating must be between 0 and 100"); - }); - - it("should only rate task ones", async function () { - const rating = 80; // Example rating - await expect(taskRegistry.connect(taskIssuer).rateTask(1, rating)) - .to.emit(taskRegistry, "TaskRated") - .withArgs(1, rating); - - await expect(taskRegistry.connect(taskIssuer).rateTask(1, rating)) - .to.be.revertedWith("Task got rating already"); - }); - }); - - describe("cancelTask", function () { - - describe('ETH tasks', function () { - beforeEach(async function () { - await setupWithEthProposal(); - - await expect(taskRegistry.createTask(prompt, proposalId, { value: taskPrice })).not.to.be.reverted - }); - - it("should cancel a task", async function () { - await expect(taskRegistry.connect(taskIssuer).cancelTask(1)) - .to.emit(taskRegistry, "TaskCanceled") - .withArgs(1); - - const updatedTask = await taskRegistry.getTask(1); - expect(updatedTask.status).to.equal(3); - }); - - it("should not cancel a task twice", async function () { - await expect(taskRegistry.connect(taskIssuer).cancelTask(1)) - .to.emit(taskRegistry, "TaskCanceled") - .withArgs(1); - - await expect(taskRegistry.connect(taskIssuer).cancelTask(1)) - .to.be.revertedWith("Task cannot be canceled"); - }); - - it("should fail if not the issuer", async function () { - await expect(taskRegistry.connect(eveAddress).cancelTask(1)) - .to.be.revertedWith("Not the issuer of the task"); - }); - }); - - describe('ERC20 tasks', function () { - beforeEach(async function () { - await setupWithErc20Proposal(); - - await mockToken.connect(taskIssuer).approve(taskRegistry.target, taskPrice); - await expect(taskRegistry.createTask(prompt, proposalId)).not.to.be.reverted; - }); - - it("should cancel an ERC20 task", async function () { - await expect(taskRegistry.connect(taskIssuer).cancelTask(1)) - .to.emit(taskRegistry, "TaskCanceled") - .withArgs(1); - - const updatedTask = await taskRegistry.getTask(1); - expect(updatedTask.status).to.equal(3); // TaskStatus.CANCELED - }); - - it("should not cancel an ERC20 task twice", async function () { - await expect(taskRegistry.connect(taskIssuer).cancelTask(1)) - .to.emit(taskRegistry, "TaskCanceled") - .withArgs(1); - - await expect(taskRegistry.connect(taskIssuer).cancelTask(1)) - .to.be.revertedWith("Task cannot be canceled"); - }); - - it("should fail if not the issuer for ERC20 task", async function () { - await expect(taskRegistry.connect(eveAddress).cancelTask(1)) - .to.be.revertedWith("Not the issuer of the task"); - }); - - it("should refund tokens to issuer when ERC20 task is canceled", async function () { - // Get balances before the task was created (need to account for the task already created in beforeEach) - const currentIssuerBalance = await mockToken.balanceOf(taskIssuer.address); - const currentContractBalance = await mockToken.balanceOf(taskRegistry.target); - - // The task was already created in beforeEach, so issuer should have paid taskPrice - // and contract should have received taskPrice - const expectedInitialIssuerBalance = currentIssuerBalance + taskPrice; - const expectedInitialContractBalance = currentContractBalance - taskPrice; - - // Verify the current state matches expectations - expect(currentIssuerBalance).to.equal(expectedInitialIssuerBalance - taskPrice); - expect(currentContractBalance).to.equal(expectedInitialContractBalance + taskPrice); - - // Cancel the task - await taskRegistry.connect(taskIssuer).cancelTask(1); - - // After cancellation, tokens should be refunded to original state - const finalIssuerBalance = await mockToken.balanceOf(taskIssuer.address); - const finalContractBalance = await mockToken.balanceOf(taskRegistry.target); - - expect(finalIssuerBalance).to.equal(expectedInitialIssuerBalance); - expect(finalContractBalance).to.equal(expectedInitialContractBalance); - }); - - it("should not allow canceling completed ERC20 task", async function () { - // Complete the task first - await taskRegistry.connect(agentAddress).completeTask(1, "Test result"); - - // Try to cancel the completed task - await expect(taskRegistry.connect(taskIssuer).cancelTask(1)) - .to.be.revertedWith("Task cannot be canceled"); - }); - }); - - }); -}); diff --git a/packages/sdk/scripts/data/servicesList.ts b/packages/sdk/scripts/data/servicesList.ts index 1abcc1d..1329cc8 100644 --- a/packages/sdk/scripts/data/servicesList.ts +++ b/packages/sdk/scripts/data/servicesList.ts @@ -1,49 +1,103 @@ -import { Service } from "../../src/types"; +import { RegisterServiceParams } from "../../src/types"; -export const servicesList: Service[] = [ +export const servicesList: RegisterServiceParams[] = [ { name: 'Bull-Post', - category: 'Social', - description: 'Bull-Post service will explain your project to the world!' + metadata: { + category: 'social', + description: 'Bull-Post service will explain your project to the world!', + endpointSchema: 'https://api.example.com/bull-post', + method: 'HTTP_POST', + parametersSchema: {}, + resultSchema: {} + } }, { name: 'Reply', - category: 'Social', - description: 'Reply agents are great for interaction and possibly farm airdrops/whitelist spots!' + metadata: { + category: 'social', + description: 'Reply agents are great for interaction and possibly farm airdrops/whitelist spots!', + endpointSchema: 'https://api.example.com/reply', + method: 'HTTP_POST', + parametersSchema: {}, + resultSchema: {} + } }, { name: 'Campaign', - category: 'Social', - description: 'Agents will run a campaign on your behalf, ensuring attention and consistency' + metadata: { + category: 'social', + description: 'Agents will run a campaign on your behalf, ensuring attention and consistency', + endpointSchema: 'https://api.example.com/campaign', + method: 'HTTP_POST', + parametersSchema: {}, + resultSchema: {} + } }, { name: 'Swap', - category: 'Defi', - description: 'Agent conducts a swap on your behalf using an optimal route with less fees' + metadata: { + category: 'defi', + description: 'Agent conducts a swap on your behalf using an optimal route with less fees', + endpointSchema: 'https://api.example.com/swap', + method: 'HTTP_POST', + parametersSchema: {}, + resultSchema: {} + } }, { name: 'Bridge', - category: 'Defi', - description: 'Agent conducts a swap on your behalf using an optimal route with less fees' + metadata: { + category: 'defi', + description: 'Agent conducts a bridge on your behalf using an optimal route with less fees', + endpointSchema: 'https://api.example.com/bridge', + method: 'HTTP_POST', + parametersSchema: {}, + resultSchema: {} + } }, { name: 'Provide LP', - category: 'Defi', - description: 'Agent conducts a swap on your behalf using an optimal route with less fees' + metadata: { + category: 'defi', + description: 'Agent provides liquidity on your behalf to earn fees and rewards', + endpointSchema: 'https://api.example.com/provide-lp', + method: 'HTTP_POST', + parametersSchema: {}, + resultSchema: {} + } }, { name: 'Markets', - category: 'Research', - description: 'Perfect for analyzing market data and providing accurate information' + metadata: { + category: 'research', + description: 'Perfect for analyzing market data and providing accurate information', + endpointSchema: 'https://api.example.com/markets', + method: 'HTTP_GET', + parametersSchema: {}, + resultSchema: {} + } }, { name: 'Trends', - category: 'Research', - description: 'Get up-tp-date with the latest trends in the Crypto world!' + metadata: { + category: 'research', + description: 'Get up-to-date with the latest trends in the Crypto world!', + endpointSchema: 'https://api.example.com/trends', + method: 'HTTP_GET', + parametersSchema: {}, + resultSchema: {} + } }, { name: 'AI Agents', - category: 'Research', - description: 'Stay updated with the latest on AI Agents!' + metadata: { + category: 'research', + description: 'Stay updated with the latest on AI Agents!', + endpointSchema: 'https://api.example.com/ai-agents', + method: 'HTTP_GET', + parametersSchema: {}, + resultSchema: {} + } } ] \ No newline at end of file diff --git a/packages/sdk/src/abi/ServiceRegistry.json b/packages/sdk/src/abi/ServiceRegistry.json index 5bed964..024208e 100644 --- a/packages/sdk/src/abi/ServiceRegistry.json +++ b/packages/sdk/src/abi/ServiceRegistry.json @@ -1,432 +1,729 @@ -[ - { - "inputs": [], - "stateMutability": "nonpayable", - "type": "constructor" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "target", - "type": "address" - } - ], - "name": "AddressEmptyCode", - "type": "error" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "implementation", - "type": "address" - } - ], - "name": "ERC1967InvalidImplementation", - "type": "error" - }, - { - "inputs": [], - "name": "ERC1967NonPayable", - "type": "error" - }, - { - "inputs": [], - "name": "FailedCall", - "type": "error" - }, - { - "inputs": [], - "name": "InvalidInitialization", - "type": "error" - }, - { - "inputs": [], - "name": "NotInitializing", - "type": "error" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "owner", - "type": "address" - } - ], - "name": "OwnableInvalidOwner", - "type": "error" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "account", - "type": "address" - } - ], - "name": "OwnableUnauthorizedAccount", - "type": "error" - }, - { - "inputs": [], - "name": "UUPSUnauthorizedCallContext", - "type": "error" - }, - { - "inputs": [ - { - "internalType": "bytes32", - "name": "slot", - "type": "bytes32" - } - ], - "name": "UUPSUnsupportedProxiableUUID", - "type": "error" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "uint64", - "name": "version", - "type": "uint64" - } - ], - "name": "Initialized", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "previousOwner", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "newOwner", - "type": "address" - } - ], - "name": "OwnershipTransferred", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "string", - "name": "name", - "type": "string" - }, - { - "indexed": false, - "internalType": "string", - "name": "category", - "type": "string" - }, - { - "indexed": false, - "internalType": "string", - "name": "description", - "type": "string" - } - ], - "name": "ServiceRegistered", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "string", - "name": "name", - "type": "string" - }, - { - "indexed": false, - "internalType": "string", - "name": "category", - "type": "string" - }, - { - "indexed": false, - "internalType": "string", - "name": "description", - "type": "string" - } - ], - "name": "ServiceUpdated", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "implementation", - "type": "address" - } - ], - "name": "Upgraded", - "type": "event" - }, - { - "inputs": [], - "name": "UPGRADE_INTERFACE_VERSION", - "outputs": [ - { - "internalType": "string", - "name": "", - "type": "string" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "string", - "name": "name", - "type": "string" - } - ], - "name": "getService", - "outputs": [ - { - "components": [ - { - "internalType": "string", - "name": "name", - "type": "string" - }, - { - "internalType": "string", - "name": "category", - "type": "string" - }, - { - "internalType": "string", - "name": "description", - "type": "string" - } - ], - "internalType": "struct ServiceRegistryUpgradeable.Service", - "name": "", - "type": "tuple" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "initialize", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "string", - "name": "name", - "type": "string" - } - ], - "name": "isServiceRegistered", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "owner", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "proxiableUUID", - "outputs": [ - { - "internalType": "bytes32", - "name": "", - "type": "bytes32" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "string", - "name": "name", - "type": "string" - }, - { - "internalType": "string", - "name": "category", - "type": "string" - }, - { - "internalType": "string", - "name": "description", - "type": "string" - } - ], - "name": "registerService", - "outputs": [ - { - "components": [ - { - "internalType": "string", - "name": "name", - "type": "string" - }, - { - "internalType": "string", - "name": "category", - "type": "string" - }, - { - "internalType": "string", - "name": "description", - "type": "string" - } - ], - "internalType": "struct ServiceRegistryUpgradeable.Service", - "name": "", - "type": "tuple" - } - ], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "renounceOwnership", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "serviceCount", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "string", - "name": "", - "type": "string" - } - ], - "name": "services", - "outputs": [ - { - "internalType": "string", - "name": "name", - "type": "string" - }, - { - "internalType": "string", - "name": "category", - "type": "string" - }, - { - "internalType": "string", - "name": "description", - "type": "string" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "newOwner", - "type": "address" - } - ], - "name": "transferOwnership", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "string", - "name": "name", - "type": "string" - }, - { - "internalType": "string", - "name": "category", - "type": "string" - }, - { - "internalType": "string", - "name": "description", - "type": "string" - } - ], - "name": "updateService", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "newImplementation", - "type": "address" - }, - { - "internalType": "bytes", - "name": "data", - "type": "bytes" - } - ], - "name": "upgradeToAndCall", - "outputs": [], - "stateMutability": "payable", - "type": "function" - } -] \ No newline at end of file +{ + "_format": "hh-sol-artifact-1", + "contractName": "ServiceRegistryUpgradeable", + "sourceName": "contracts/ServiceRegistryUpgradeable.sol", + "abi": [ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "target", + "type": "address" + } + ], + "name": "AddressEmptyCode", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "implementation", + "type": "address" + } + ], + "name": "ERC1967InvalidImplementation", + "type": "error" + }, + { + "inputs": [], + "name": "ERC1967NonPayable", + "type": "error" + }, + { + "inputs": [], + "name": "FailedCall", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidInitialization", + "type": "error" + }, + { + "inputs": [], + "name": "NotInitializing", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "OwnableInvalidOwner", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "OwnableUnauthorizedAccount", + "type": "error" + }, + { + "inputs": [], + "name": "UUPSUnauthorizedCallContext", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "slot", + "type": "bytes32" + } + ], + "name": "UUPSUnsupportedProxiableUUID", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64", + "name": "version", + "type": "uint64" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "serviceId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "agent", + "type": "address" + } + ], + "name": "ServiceAgentAssigned", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "serviceId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "agent", + "type": "address" + } + ], + "name": "ServiceAgentUnassigned", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "serviceId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "oldOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "ServiceOwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "serviceId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "internalType": "string", + "name": "serviceUri", + "type": "string" + } + ], + "name": "ServiceRegistered", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "serviceId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "enum ServiceRegistryUpgradeable.ServiceStatus", + "name": "oldStatus", + "type": "uint8" + }, + { + "indexed": true, + "internalType": "enum ServiceRegistryUpgradeable.ServiceStatus", + "name": "newStatus", + "type": "uint8" + } + ], + "name": "ServiceStatusChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "serviceId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "string", + "name": "serviceUri", + "type": "string" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "version", + "type": "uint256" + } + ], + "name": "ServiceUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "implementation", + "type": "address" + } + ], + "name": "Upgraded", + "type": "event" + }, + { + "inputs": [], + "name": "UPGRADE_INTERFACE_VERSION", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "serviceId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "agent", + "type": "address" + } + ], + "name": "assignAgentToService", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "serviceId", + "type": "uint256" + } + ], + "name": "deleteService", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "serviceId", + "type": "uint256" + } + ], + "name": "getService", + "outputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "agentAddress", + "type": "address" + }, + { + "internalType": "string", + "name": "serviceUri", + "type": "string" + }, + { + "internalType": "enum ServiceRegistryUpgradeable.ServiceStatus", + "name": "status", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "version", + "type": "uint256" + } + ], + "internalType": "struct ServiceRegistryUpgradeable.Service", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "serviceId", + "type": "uint256" + } + ], + "name": "getServiceOwner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "agent", + "type": "address" + } + ], + "name": "getServicesByAgent", + "outputs": [ + { + "internalType": "uint256[]", + "name": "", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "getServicesByOwner", + "outputs": [ + { + "internalType": "uint256[]", + "name": "", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getTotalServiceCount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "nextServiceId", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "proxiableUUID", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "serviceUri", + "type": "string" + }, + { + "internalType": "address", + "name": "agentAddress", + "type": "address" + } + ], + "name": "registerService", + "outputs": [ + { + "internalType": "uint256", + "name": "serviceId", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "services", + "outputs": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "agentAddress", + "type": "address" + }, + { + "internalType": "string", + "name": "serviceUri", + "type": "string" + }, + { + "internalType": "enum ServiceRegistryUpgradeable.ServiceStatus", + "name": "status", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "version", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "servicesByAgent", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "servicesByOwner", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalServices", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "serviceId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferServiceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "serviceId", + "type": "uint256" + }, + { + "internalType": "enum ServiceRegistryUpgradeable.ServiceStatus", + "name": "newStatus", + "type": "uint8" + } + ], + "name": "unassignAgentFromService", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "serviceId", + "type": "uint256" + }, + { + "internalType": "string", + "name": "serviceUri", + "type": "string" + } + ], + "name": "updateService", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "serviceId", + "type": "uint256" + }, + { + "internalType": "enum ServiceRegistryUpgradeable.ServiceStatus", + "name": "newStatus", + "type": "uint8" + } + ], + "name": "updateServiceStatus", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "upgradeToAndCall", + "outputs": [], + "stateMutability": "payable", + "type": "function" + } + ], + "bytecode": "0x60a06040523060805234801561001457600080fd5b5061001d610022565b6100d4565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00805468010000000000000000900460ff16156100725760405163f92ee8a960e01b815260040160405180910390fd5b80546001600160401b03908116146100d15780546001600160401b0319166001600160401b0390811782556040519081527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29060200160405180910390a15b50565b6080516122bb620000fe600039600081816116220152818161164b015261179101526122bb6000f3fe60806040526004361061014b5760003560e01c80638129fc1c116100b6578063ad3cb1cc1161006f578063ad3cb1cc146103a8578063b18ba34c146103e6578063c22c4f4314610406578063ef0e239b14610438578063f2fde38b14610465578063f6c53d9b1461048557600080fd5b80638129fc1c146102d65780638da5cb5b146102eb57806394f6953c14610328578063a4c258d214610348578063a9c583a414610368578063aa0a60e31461038857600080fd5b80634f6753be116101085780634f6753be1461023657806352d1902d1461025657806360423fa21461026b5780636301ac8f1461028b578063715018a6146102a157806374e29ee6146102b657600080fd5b8063142844d9146101505780631b81aaea146101865780632336f57a146101be5780633573c000146101e057806347e905b41461020e5780634f1ef28614610223575b600080fd5b34801561015c57600080fd5b5061017061016b366004611c11565b61049b565b60405161017d9190611c2c565b60405180910390f35b34801561019257600080fd5b506101a66101a1366004611c70565b610507565b6040516001600160a01b03909116815260200161017d565b3480156101ca57600080fd5b506101de6101d9366004611c89565b610565565b005b3480156101ec57600080fd5b506102006101fb366004611d69565b610796565b60405190815260200161017d565b34801561021a57600080fd5b50600454610200565b6101de610231366004611db7565b6109b7565b34801561024257600080fd5b50610200610251366004611e19565b6109d6565b34801561026257600080fd5b50610200610a07565b34801561027757600080fd5b506101de610286366004611c89565b610a24565b34801561029757600080fd5b5061020060045481565b3480156102ad57600080fd5b506101de610bb5565b3480156102c257600080fd5b506101de6102d1366004611c70565b610bc9565b3480156102e257600080fd5b506101de610d02565b3480156102f757600080fd5b507f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300546001600160a01b03166101a6565b34801561033457600080fd5b506101de610343366004611e43565b610e0f565b34801561035457600080fd5b50610170610363366004611c11565b610fa0565b34801561037457600080fd5b506101de610383366004611e66565b61100a565b34801561039457600080fd5b506102006103a3366004611e19565b611155565b3480156103b457600080fd5b506103d9604051806040016040528060058152602001640352e302e360dc1b81525081565b60405161017d9190611ef3565b3480156103f257600080fd5b506101de610401366004611e43565b611171565b34801561041257600080fd5b50610426610421366004611c70565b6112bf565b60405161017d96959493929190611f3e565b34801561044457600080fd5b50610458610453366004611c70565b61138e565b60405161017d9190611f90565b34801561047157600080fd5b506101de610480366004611c11565b61150f565b34801561049157600080fd5b5061020060035481565b6001600160a01b0381166000908152600160209081526040918290208054835181840281018401909452808452606093928301828280156104fb57602002820191906000526020600020905b8154815260200190600101908083116104e7575b50505050509050919050565b60008160008111801561051c57506003548111155b6105415760405162461bcd60e51b815260040161053890612001565b60405180910390fd5b6000838152602081905260409020600101546001600160a01b031691505b50919050565b8160008111801561057857506003548111155b6105945760405162461bcd60e51b815260040161053890612001565b60008381526020819052604090206001015483906001600160a01b031633146105cf5760405162461bcd60e51b815260040161053890612031565b60008360038111156105e3576105e3611f06565b1480610600575060028360038111156105fe576105fe611f06565b145b61064c5760405162461bcd60e51b815260206004820181905260248201527f537461747573206d757374206265204452414654206f722041524348495645446044820152606401610538565b600084815260208190526040902060028101546001600160a01b0316806106a95760405162461bcd60e51b8152602060048201526011602482015270139bc81859d95b9d08185cdcda59db9959607a1b6044820152606401610538565b6002820180546001600160a01b031916905560048201805486919060ff191660018360038111156106dc576106dc611f06565b02179055506005820180549060006106f383612072565b9190505550610702818761154d565b6040516001600160a01b0382169087907f47d159ee6cd4a9661a2396173c57e582543a45dd368ee7bf5e9d261a1f5bfcf290600090a384600381111561074a5761074a611f06565b600483015460ff16600381111561076357610763611f06565b60405188907fed930c5294841f355eada664af337ef1973538faea7f1eacfa8ed18d8e28ed2590600090a4505050505050565b6000808351116107df5760405162461bcd60e51b815260206004820152601460248201527314d95c9d9a58d948155492481c995c5d5a5c995960621b6044820152606401610538565b6003600081546107ee90612072565b91829055506040805160c0810182528281523360208083019182526001600160a01b03878116848601908152606085018a8152600060808701819052600160a08801819052898252948190529690962085518155935192840180546001600160a01b03199081169484169490941790555160028401805490931691161790559151929350916003820190610882908261210f565b50608082015160048201805460ff191660018360038111156108a6576108a6611f06565b021790555060a091909101516005909101553360009081526001602081815260408320805492830181558352909120018190556001600160a01b03821615610914576001600160a01b0382166000908152600260209081526040822080546001810182559083529120018190555b6004805490600061092483612072565b9190505550336001600160a01b0316817f4b92d29535f834801f0d689cb5c3e2e5686af3c1255295ac3395351a6eccab9e856040516109639190611ef3565b60405180910390a36001600160a01b038216156109b1576040516001600160a01b0383169082907fb0653f9e2f8c4289f8f8595176f1bd9fbd910a82a95f4d92c5f4cd4755538ef590600090a35b92915050565b6109bf611617565b6109c8826116bc565b6109d282826116c4565b5050565b600160205281600052604060002081815481106109f257600080fd5b90600052602060002001600091509150505481565b6000610a11611786565b5060008051602061226683398151915290565b81600081118015610a3757506003548111155b610a535760405162461bcd60e51b815260040161053890612001565b60008381526020819052604090206001015483906001600160a01b03163314610a8e5760405162461bcd60e51b815260040161053890612031565b6000848152602081905260409020600481015460ff166003816003811115610ab857610ab8611f06565b03610b155760405162461bcd60e51b815260206004820152602760248201527f43616e6e6f74206368616e676520737461747573206f662064656c65746564206044820152667365727669636560c81b6064820152608401610538565b610b1f81866117cf565b610b6b5760405162461bcd60e51b815260206004820152601960248201527f496e76616c696420737461747573207472616e736974696f6e000000000000006044820152606401610538565b60048201805486919060ff19166001836003811115610b8c57610b8c611f06565b0217905550846003811115610ba357610ba3611f06565b81600381111561076357610763611f06565b610bbd61186b565b610bc760006118c6565b565b80600081118015610bdc57506003548111155b610bf85760405162461bcd60e51b815260040161053890612001565b60008281526020819052604090206001015482906001600160a01b03163314610c335760405162461bcd60e51b815260040161053890612031565b60008381526020819052604090206003600482015460ff166003811115610c5c57610c5c611f06565b03610ca95760405162461bcd60e51b815260206004820152601760248201527f5365727669636520616c72656164792064656c657465640000000000000000006044820152606401610538565b600481018054600360ff198216811790925560ff16908181811115610cd057610cd0611f06565b60405187907fed930c5294841f355eada664af337ef1973538faea7f1eacfa8ed18d8e28ed2590600090a45050505050565b6000610d0c611937565b805490915060ff600160401b820416159067ffffffffffffffff16600081158015610d345750825b905060008267ffffffffffffffff166001148015610d515750303b155b905081158015610d5f575080155b15610d7d5760405163f92ee8a960e01b815260040160405180910390fd5b845467ffffffffffffffff191660011785558315610da757845460ff60401b1916600160401b1785555b610db033611960565b610db8611971565b600060038190556004558315610e0857845460ff60401b19168555604051600181527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29060200160405180910390a15b5050505050565b81600081118015610e2257506003548111155b610e3e5760405162461bcd60e51b815260040161053890612001565b60008381526020819052604090206001015483906001600160a01b03163314610e795760405162461bcd60e51b815260040161053890612031565b6001600160a01b038316610ec75760405162461bcd60e51b8152602060048201526015602482015274496e76616c6964206167656e74206164647265737360581b6044820152606401610538565b600084815260208190526040902060028101546001600160a01b03168015610f2a57610ef3818761154d565b6040516001600160a01b0382169087907f47d159ee6cd4a9661a2396173c57e582543a45dd368ee7bf5e9d261a1f5bfcf290600090a35b600282810180546001600160a01b0319166001600160a01b03881690811790915560008181526020928352604080822080546001810182559083529382209093018990559151909188917fb0653f9e2f8c4289f8f8595176f1bd9fbd910a82a95f4d92c5f4cd4755538ef59190a3505050505050565b6001600160a01b0381166000908152600260209081526040918290208054835181840281018401909452808452606093928301828280156104fb57602002820191906000526020600020908154815260200190600101908083116104e75750505050509050919050565b8160008111801561101d57506003548111155b6110395760405162461bcd60e51b815260040161053890612001565b60008381526020819052604090206001015483906001600160a01b031633146110745760405162461bcd60e51b815260040161053890612031565b60008481526020819052604090206003600482015460ff16600381111561109d5761109d611f06565b036110ea5760405162461bcd60e51b815260206004820152601d60248201527f43616e6e6f74207570646174652064656c6574656420736572766963650000006044820152606401610538565b600381016110f8858261210f565b5060058101805490600061110b83612072565b9190505550847fae55ce3c351e2400a1a143e7ed66668e581e1656ad1797a317e415d0358809168583600501546040516111469291906121cf565b60405180910390a25050505050565b600260205281600052604060002081815481106109f257600080fd5b8160008111801561118457506003548111155b6111a05760405162461bcd60e51b815260040161053890612001565b60008381526020819052604090206001015483906001600160a01b031633146111db5760405162461bcd60e51b815260040161053890612031565b6001600160a01b0383166112255760405162461bcd60e51b815260206004820152601160248201527024b73b30b634b2103732bb9037bbb732b960791b6044820152606401610538565b60008481526020819052604090206001810180546001600160a01b038681166001600160a01b03198316179092551661125e8187611979565b6001600160a01b0380861660008181526001602081815260408084208054938401815584529083209091018a905551919284169189917fe6997a85d3096b35434817efd111191444e46c8b0cce53862a2d8d43ff5a33df91a4505050505050565b600060208190529081526040902080546001820154600283015460038401805493946001600160a01b039384169493909216926112fb9061208b565b80601f01602080910402602001604051908101604052809291908181526020018280546113279061208b565b80156113745780601f1061134957610100808354040283529160200191611374565b820191906000526020600020905b81548152906001019060200180831161135757829003601f168201915b505050506004830154600590930154919260ff1691905086565b6040805160c0810182526000808252602082018190529181018290526060808201526080810182905260a0810191909152816000811180156113d257506003548111155b6113ee5760405162461bcd60e51b815260040161053890612001565b60008381526020818152604091829020825160c0810184528154815260018201546001600160a01b03908116938201939093526002820154909216928201929092526003820180549192916060840191906114489061208b565b80601f01602080910402602001604051908101604052809291908181526020018280546114749061208b565b80156114c15780601f10611496576101008083540402835291602001916114c1565b820191906000526020600020905b8154815290600101906020018083116114a457829003601f168201915b5050509183525050600482015460209091019060ff1660038111156114e8576114e8611f06565b60038111156114f9576114f9611f06565b8152602001600582015481525050915050919050565b61151761186b565b6001600160a01b03811661154157604051631e4fbdf760e01b815260006004820152602401610538565b61154a816118c6565b50565b6001600160a01b0382166000908152600260205260408120905b81548110156116115782828281548110611583576115836121f1565b90600052602060002001540361160957815482906115a390600190612207565b815481106115b3576115b36121f1565b90600052602060002001548282815481106115d0576115d06121f1565b9060005260206000200181905550818054806115ee576115ee61221a565b60019003818190600052602060002001600090559055611611565b600101611567565b50505050565b306001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016148061169e57507f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316611692600080516020612266833981519152546001600160a01b031690565b6001600160a01b031614155b15610bc75760405163703e46dd60e11b815260040160405180910390fd5b61154a61186b565b816001600160a01b03166352d1902d6040518163ffffffff1660e01b8152600401602060405180830381865afa92505050801561171e575060408051601f3d908101601f1916820190925261171b91810190612230565b60015b61174657604051634c9c8ce360e01b81526001600160a01b0383166004820152602401610538565b600080516020612266833981519152811461177757604051632a87526960e21b815260048101829052602401610538565b61178183836119d7565b505050565b306001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001614610bc75760405163703e46dd60e11b815260040160405180910390fd5b6000808360038111156117e4576117e4611f06565b036118225760015b8260038111156117fe576117fe611f06565b148061181b5750600382600381111561181957611819611f06565b145b90506109b1565b600183600381111561183657611836611f06565b036118425760026117ec565b600283600381111561185657611856611f06565b036118625760016117ec565b50600092915050565b3361189d7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300546001600160a01b031690565b6001600160a01b031614610bc75760405163118cdaa760e01b8152336004820152602401610538565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c19930080546001600160a01b031981166001600160a01b03848116918217845560405192169182907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e090600090a3505050565b6000807ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a006109b1565b611968611a2d565b61154a81611a52565b610bc7611a2d565b6001600160a01b0382166000908152600160205260408120905b815481101561161157828282815481106119af576119af6121f1565b9060005260206000200154036119cf57815482906115a390600190612207565b600101611993565b6119e082611a5a565b6040516001600160a01b038316907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b90600090a2805115611a25576117818282611abf565b6109d2611b35565b611a35611b54565b610bc757604051631afcd79f60e31b815260040160405180910390fd5b611517611a2d565b806001600160a01b03163b600003611a9057604051634c9c8ce360e01b81526001600160a01b0382166004820152602401610538565b60008051602061226683398151915280546001600160a01b0319166001600160a01b0392909216919091179055565b6060600080846001600160a01b031684604051611adc9190612249565b600060405180830381855af49150503d8060008114611b17576040519150601f19603f3d011682016040523d82523d6000602084013e611b1c565b606091505b5091509150611b2c858383611b6e565b95945050505050565b3415610bc75760405163b398979f60e01b815260040160405180910390fd5b6000611b5e611937565b54600160401b900460ff16919050565b606082611b8357611b7e82611bcd565b611bc6565b8151158015611b9a57506001600160a01b0384163b155b15611bc357604051639996b31560e01b81526001600160a01b0385166004820152602401610538565b50805b9392505050565b805115611bdc57805160208201fd5b60405163d6bda27560e01b815260040160405180910390fd5b80356001600160a01b0381168114611c0c57600080fd5b919050565b600060208284031215611c2357600080fd5b611bc682611bf5565b6020808252825182820181905260009190848201906040850190845b81811015611c6457835183529284019291840191600101611c48565b50909695505050505050565b600060208284031215611c8257600080fd5b5035919050565b60008060408385031215611c9c57600080fd5b82359150602083013560048110611cb257600080fd5b809150509250929050565b634e487b7160e01b600052604160045260246000fd5b600067ffffffffffffffff80841115611cee57611cee611cbd565b604051601f8501601f19908116603f01168101908282118183101715611d1657611d16611cbd565b81604052809350858152868686011115611d2f57600080fd5b858560208301376000602087830101525050509392505050565b600082601f830112611d5a57600080fd5b611bc683833560208501611cd3565b60008060408385031215611d7c57600080fd5b823567ffffffffffffffff811115611d9357600080fd5b611d9f85828601611d49565b925050611dae60208401611bf5565b90509250929050565b60008060408385031215611dca57600080fd5b611dd383611bf5565b9150602083013567ffffffffffffffff811115611def57600080fd5b8301601f81018513611e0057600080fd5b611e0f85823560208401611cd3565b9150509250929050565b60008060408385031215611e2c57600080fd5b611e3583611bf5565b946020939093013593505050565b60008060408385031215611e5657600080fd5b82359150611dae60208401611bf5565b60008060408385031215611e7957600080fd5b82359150602083013567ffffffffffffffff811115611e9757600080fd5b611e0f85828601611d49565b60005b83811015611ebe578181015183820152602001611ea6565b50506000910152565b60008151808452611edf816020860160208601611ea3565b601f01601f19169290920160200192915050565b602081526000611bc66020830184611ec7565b634e487b7160e01b600052602160045260246000fd5b60048110611f3a57634e487b7160e01b600052602160045260246000fd5b9052565b8681526001600160a01b0386811660208301528516604082015260c060608201819052600090611f7090830186611ec7565b9050611f7f6080830185611f1c565b8260a0830152979650505050505050565b60208152815160208201526000602083015160018060a01b0380821660408501528060408601511660608501525050606083015160c06080840152611fd860e0840182611ec7565b90506080840151611fec60a0850182611f1c565b5060a084015160c08401528091505092915050565b60208082526016908201527514d95c9d9a58d948191bd95cc81b9bdd08195e1a5cdd60521b604082015260600190565b6020808252601190820152702737ba1039b2b93b34b1b29037bbb732b960791b604082015260600190565b634e487b7160e01b600052601160045260246000fd5b6000600182016120845761208461205c565b5060010190565b600181811c9082168061209f57607f821691505b60208210810361055f57634e487b7160e01b600052602260045260246000fd5b601f821115611781576000816000526020600020601f850160051c810160208610156120e85750805b601f850160051c820191505b81811015612107578281556001016120f4565b505050505050565b815167ffffffffffffffff81111561212957612129611cbd565b61213d81612137845461208b565b846120bf565b602080601f831160018114612172576000841561215a5750858301515b600019600386901b1c1916600185901b178555612107565b600085815260208120601f198616915b828110156121a157888601518255948401946001909101908401612182565b50858210156121bf5787850151600019600388901b60f8161c191681555b5050505050600190811b01905550565b6040815260006121e26040830185611ec7565b90508260208301529392505050565b634e487b7160e01b600052603260045260246000fd5b818103818111156109b1576109b161205c565b634e487b7160e01b600052603160045260246000fd5b60006020828403121561224257600080fd5b5051919050565b6000825161225b818460208701611ea3565b919091019291505056fe360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbca2646970667358221220d9549bd0f79d88e74336e4e604a702e758534a1e4549564f637caba37d89fc3464736f6c63430008160033", + "deployedBytecode": "0x60806040526004361061014b5760003560e01c80638129fc1c116100b6578063ad3cb1cc1161006f578063ad3cb1cc146103a8578063b18ba34c146103e6578063c22c4f4314610406578063ef0e239b14610438578063f2fde38b14610465578063f6c53d9b1461048557600080fd5b80638129fc1c146102d65780638da5cb5b146102eb57806394f6953c14610328578063a4c258d214610348578063a9c583a414610368578063aa0a60e31461038857600080fd5b80634f6753be116101085780634f6753be1461023657806352d1902d1461025657806360423fa21461026b5780636301ac8f1461028b578063715018a6146102a157806374e29ee6146102b657600080fd5b8063142844d9146101505780631b81aaea146101865780632336f57a146101be5780633573c000146101e057806347e905b41461020e5780634f1ef28614610223575b600080fd5b34801561015c57600080fd5b5061017061016b366004611c11565b61049b565b60405161017d9190611c2c565b60405180910390f35b34801561019257600080fd5b506101a66101a1366004611c70565b610507565b6040516001600160a01b03909116815260200161017d565b3480156101ca57600080fd5b506101de6101d9366004611c89565b610565565b005b3480156101ec57600080fd5b506102006101fb366004611d69565b610796565b60405190815260200161017d565b34801561021a57600080fd5b50600454610200565b6101de610231366004611db7565b6109b7565b34801561024257600080fd5b50610200610251366004611e19565b6109d6565b34801561026257600080fd5b50610200610a07565b34801561027757600080fd5b506101de610286366004611c89565b610a24565b34801561029757600080fd5b5061020060045481565b3480156102ad57600080fd5b506101de610bb5565b3480156102c257600080fd5b506101de6102d1366004611c70565b610bc9565b3480156102e257600080fd5b506101de610d02565b3480156102f757600080fd5b507f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300546001600160a01b03166101a6565b34801561033457600080fd5b506101de610343366004611e43565b610e0f565b34801561035457600080fd5b50610170610363366004611c11565b610fa0565b34801561037457600080fd5b506101de610383366004611e66565b61100a565b34801561039457600080fd5b506102006103a3366004611e19565b611155565b3480156103b457600080fd5b506103d9604051806040016040528060058152602001640352e302e360dc1b81525081565b60405161017d9190611ef3565b3480156103f257600080fd5b506101de610401366004611e43565b611171565b34801561041257600080fd5b50610426610421366004611c70565b6112bf565b60405161017d96959493929190611f3e565b34801561044457600080fd5b50610458610453366004611c70565b61138e565b60405161017d9190611f90565b34801561047157600080fd5b506101de610480366004611c11565b61150f565b34801561049157600080fd5b5061020060035481565b6001600160a01b0381166000908152600160209081526040918290208054835181840281018401909452808452606093928301828280156104fb57602002820191906000526020600020905b8154815260200190600101908083116104e7575b50505050509050919050565b60008160008111801561051c57506003548111155b6105415760405162461bcd60e51b815260040161053890612001565b60405180910390fd5b6000838152602081905260409020600101546001600160a01b031691505b50919050565b8160008111801561057857506003548111155b6105945760405162461bcd60e51b815260040161053890612001565b60008381526020819052604090206001015483906001600160a01b031633146105cf5760405162461bcd60e51b815260040161053890612031565b60008360038111156105e3576105e3611f06565b1480610600575060028360038111156105fe576105fe611f06565b145b61064c5760405162461bcd60e51b815260206004820181905260248201527f537461747573206d757374206265204452414654206f722041524348495645446044820152606401610538565b600084815260208190526040902060028101546001600160a01b0316806106a95760405162461bcd60e51b8152602060048201526011602482015270139bc81859d95b9d08185cdcda59db9959607a1b6044820152606401610538565b6002820180546001600160a01b031916905560048201805486919060ff191660018360038111156106dc576106dc611f06565b02179055506005820180549060006106f383612072565b9190505550610702818761154d565b6040516001600160a01b0382169087907f47d159ee6cd4a9661a2396173c57e582543a45dd368ee7bf5e9d261a1f5bfcf290600090a384600381111561074a5761074a611f06565b600483015460ff16600381111561076357610763611f06565b60405188907fed930c5294841f355eada664af337ef1973538faea7f1eacfa8ed18d8e28ed2590600090a4505050505050565b6000808351116107df5760405162461bcd60e51b815260206004820152601460248201527314d95c9d9a58d948155492481c995c5d5a5c995960621b6044820152606401610538565b6003600081546107ee90612072565b91829055506040805160c0810182528281523360208083019182526001600160a01b03878116848601908152606085018a8152600060808701819052600160a08801819052898252948190529690962085518155935192840180546001600160a01b03199081169484169490941790555160028401805490931691161790559151929350916003820190610882908261210f565b50608082015160048201805460ff191660018360038111156108a6576108a6611f06565b021790555060a091909101516005909101553360009081526001602081815260408320805492830181558352909120018190556001600160a01b03821615610914576001600160a01b0382166000908152600260209081526040822080546001810182559083529120018190555b6004805490600061092483612072565b9190505550336001600160a01b0316817f4b92d29535f834801f0d689cb5c3e2e5686af3c1255295ac3395351a6eccab9e856040516109639190611ef3565b60405180910390a36001600160a01b038216156109b1576040516001600160a01b0383169082907fb0653f9e2f8c4289f8f8595176f1bd9fbd910a82a95f4d92c5f4cd4755538ef590600090a35b92915050565b6109bf611617565b6109c8826116bc565b6109d282826116c4565b5050565b600160205281600052604060002081815481106109f257600080fd5b90600052602060002001600091509150505481565b6000610a11611786565b5060008051602061226683398151915290565b81600081118015610a3757506003548111155b610a535760405162461bcd60e51b815260040161053890612001565b60008381526020819052604090206001015483906001600160a01b03163314610a8e5760405162461bcd60e51b815260040161053890612031565b6000848152602081905260409020600481015460ff166003816003811115610ab857610ab8611f06565b03610b155760405162461bcd60e51b815260206004820152602760248201527f43616e6e6f74206368616e676520737461747573206f662064656c65746564206044820152667365727669636560c81b6064820152608401610538565b610b1f81866117cf565b610b6b5760405162461bcd60e51b815260206004820152601960248201527f496e76616c696420737461747573207472616e736974696f6e000000000000006044820152606401610538565b60048201805486919060ff19166001836003811115610b8c57610b8c611f06565b0217905550846003811115610ba357610ba3611f06565b81600381111561076357610763611f06565b610bbd61186b565b610bc760006118c6565b565b80600081118015610bdc57506003548111155b610bf85760405162461bcd60e51b815260040161053890612001565b60008281526020819052604090206001015482906001600160a01b03163314610c335760405162461bcd60e51b815260040161053890612031565b60008381526020819052604090206003600482015460ff166003811115610c5c57610c5c611f06565b03610ca95760405162461bcd60e51b815260206004820152601760248201527f5365727669636520616c72656164792064656c657465640000000000000000006044820152606401610538565b600481018054600360ff198216811790925560ff16908181811115610cd057610cd0611f06565b60405187907fed930c5294841f355eada664af337ef1973538faea7f1eacfa8ed18d8e28ed2590600090a45050505050565b6000610d0c611937565b805490915060ff600160401b820416159067ffffffffffffffff16600081158015610d345750825b905060008267ffffffffffffffff166001148015610d515750303b155b905081158015610d5f575080155b15610d7d5760405163f92ee8a960e01b815260040160405180910390fd5b845467ffffffffffffffff191660011785558315610da757845460ff60401b1916600160401b1785555b610db033611960565b610db8611971565b600060038190556004558315610e0857845460ff60401b19168555604051600181527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29060200160405180910390a15b5050505050565b81600081118015610e2257506003548111155b610e3e5760405162461bcd60e51b815260040161053890612001565b60008381526020819052604090206001015483906001600160a01b03163314610e795760405162461bcd60e51b815260040161053890612031565b6001600160a01b038316610ec75760405162461bcd60e51b8152602060048201526015602482015274496e76616c6964206167656e74206164647265737360581b6044820152606401610538565b600084815260208190526040902060028101546001600160a01b03168015610f2a57610ef3818761154d565b6040516001600160a01b0382169087907f47d159ee6cd4a9661a2396173c57e582543a45dd368ee7bf5e9d261a1f5bfcf290600090a35b600282810180546001600160a01b0319166001600160a01b03881690811790915560008181526020928352604080822080546001810182559083529382209093018990559151909188917fb0653f9e2f8c4289f8f8595176f1bd9fbd910a82a95f4d92c5f4cd4755538ef59190a3505050505050565b6001600160a01b0381166000908152600260209081526040918290208054835181840281018401909452808452606093928301828280156104fb57602002820191906000526020600020908154815260200190600101908083116104e75750505050509050919050565b8160008111801561101d57506003548111155b6110395760405162461bcd60e51b815260040161053890612001565b60008381526020819052604090206001015483906001600160a01b031633146110745760405162461bcd60e51b815260040161053890612031565b60008481526020819052604090206003600482015460ff16600381111561109d5761109d611f06565b036110ea5760405162461bcd60e51b815260206004820152601d60248201527f43616e6e6f74207570646174652064656c6574656420736572766963650000006044820152606401610538565b600381016110f8858261210f565b5060058101805490600061110b83612072565b9190505550847fae55ce3c351e2400a1a143e7ed66668e581e1656ad1797a317e415d0358809168583600501546040516111469291906121cf565b60405180910390a25050505050565b600260205281600052604060002081815481106109f257600080fd5b8160008111801561118457506003548111155b6111a05760405162461bcd60e51b815260040161053890612001565b60008381526020819052604090206001015483906001600160a01b031633146111db5760405162461bcd60e51b815260040161053890612031565b6001600160a01b0383166112255760405162461bcd60e51b815260206004820152601160248201527024b73b30b634b2103732bb9037bbb732b960791b6044820152606401610538565b60008481526020819052604090206001810180546001600160a01b038681166001600160a01b03198316179092551661125e8187611979565b6001600160a01b0380861660008181526001602081815260408084208054938401815584529083209091018a905551919284169189917fe6997a85d3096b35434817efd111191444e46c8b0cce53862a2d8d43ff5a33df91a4505050505050565b600060208190529081526040902080546001820154600283015460038401805493946001600160a01b039384169493909216926112fb9061208b565b80601f01602080910402602001604051908101604052809291908181526020018280546113279061208b565b80156113745780601f1061134957610100808354040283529160200191611374565b820191906000526020600020905b81548152906001019060200180831161135757829003601f168201915b505050506004830154600590930154919260ff1691905086565b6040805160c0810182526000808252602082018190529181018290526060808201526080810182905260a0810191909152816000811180156113d257506003548111155b6113ee5760405162461bcd60e51b815260040161053890612001565b60008381526020818152604091829020825160c0810184528154815260018201546001600160a01b03908116938201939093526002820154909216928201929092526003820180549192916060840191906114489061208b565b80601f01602080910402602001604051908101604052809291908181526020018280546114749061208b565b80156114c15780601f10611496576101008083540402835291602001916114c1565b820191906000526020600020905b8154815290600101906020018083116114a457829003601f168201915b5050509183525050600482015460209091019060ff1660038111156114e8576114e8611f06565b60038111156114f9576114f9611f06565b8152602001600582015481525050915050919050565b61151761186b565b6001600160a01b03811661154157604051631e4fbdf760e01b815260006004820152602401610538565b61154a816118c6565b50565b6001600160a01b0382166000908152600260205260408120905b81548110156116115782828281548110611583576115836121f1565b90600052602060002001540361160957815482906115a390600190612207565b815481106115b3576115b36121f1565b90600052602060002001548282815481106115d0576115d06121f1565b9060005260206000200181905550818054806115ee576115ee61221a565b60019003818190600052602060002001600090559055611611565b600101611567565b50505050565b306001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016148061169e57507f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316611692600080516020612266833981519152546001600160a01b031690565b6001600160a01b031614155b15610bc75760405163703e46dd60e11b815260040160405180910390fd5b61154a61186b565b816001600160a01b03166352d1902d6040518163ffffffff1660e01b8152600401602060405180830381865afa92505050801561171e575060408051601f3d908101601f1916820190925261171b91810190612230565b60015b61174657604051634c9c8ce360e01b81526001600160a01b0383166004820152602401610538565b600080516020612266833981519152811461177757604051632a87526960e21b815260048101829052602401610538565b61178183836119d7565b505050565b306001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001614610bc75760405163703e46dd60e11b815260040160405180910390fd5b6000808360038111156117e4576117e4611f06565b036118225760015b8260038111156117fe576117fe611f06565b148061181b5750600382600381111561181957611819611f06565b145b90506109b1565b600183600381111561183657611836611f06565b036118425760026117ec565b600283600381111561185657611856611f06565b036118625760016117ec565b50600092915050565b3361189d7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300546001600160a01b031690565b6001600160a01b031614610bc75760405163118cdaa760e01b8152336004820152602401610538565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c19930080546001600160a01b031981166001600160a01b03848116918217845560405192169182907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e090600090a3505050565b6000807ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a006109b1565b611968611a2d565b61154a81611a52565b610bc7611a2d565b6001600160a01b0382166000908152600160205260408120905b815481101561161157828282815481106119af576119af6121f1565b9060005260206000200154036119cf57815482906115a390600190612207565b600101611993565b6119e082611a5a565b6040516001600160a01b038316907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b90600090a2805115611a25576117818282611abf565b6109d2611b35565b611a35611b54565b610bc757604051631afcd79f60e31b815260040160405180910390fd5b611517611a2d565b806001600160a01b03163b600003611a9057604051634c9c8ce360e01b81526001600160a01b0382166004820152602401610538565b60008051602061226683398151915280546001600160a01b0319166001600160a01b0392909216919091179055565b6060600080846001600160a01b031684604051611adc9190612249565b600060405180830381855af49150503d8060008114611b17576040519150601f19603f3d011682016040523d82523d6000602084013e611b1c565b606091505b5091509150611b2c858383611b6e565b95945050505050565b3415610bc75760405163b398979f60e01b815260040160405180910390fd5b6000611b5e611937565b54600160401b900460ff16919050565b606082611b8357611b7e82611bcd565b611bc6565b8151158015611b9a57506001600160a01b0384163b155b15611bc357604051639996b31560e01b81526001600160a01b0385166004820152602401610538565b50805b9392505050565b805115611bdc57805160208201fd5b60405163d6bda27560e01b815260040160405180910390fd5b80356001600160a01b0381168114611c0c57600080fd5b919050565b600060208284031215611c2357600080fd5b611bc682611bf5565b6020808252825182820181905260009190848201906040850190845b81811015611c6457835183529284019291840191600101611c48565b50909695505050505050565b600060208284031215611c8257600080fd5b5035919050565b60008060408385031215611c9c57600080fd5b82359150602083013560048110611cb257600080fd5b809150509250929050565b634e487b7160e01b600052604160045260246000fd5b600067ffffffffffffffff80841115611cee57611cee611cbd565b604051601f8501601f19908116603f01168101908282118183101715611d1657611d16611cbd565b81604052809350858152868686011115611d2f57600080fd5b858560208301376000602087830101525050509392505050565b600082601f830112611d5a57600080fd5b611bc683833560208501611cd3565b60008060408385031215611d7c57600080fd5b823567ffffffffffffffff811115611d9357600080fd5b611d9f85828601611d49565b925050611dae60208401611bf5565b90509250929050565b60008060408385031215611dca57600080fd5b611dd383611bf5565b9150602083013567ffffffffffffffff811115611def57600080fd5b8301601f81018513611e0057600080fd5b611e0f85823560208401611cd3565b9150509250929050565b60008060408385031215611e2c57600080fd5b611e3583611bf5565b946020939093013593505050565b60008060408385031215611e5657600080fd5b82359150611dae60208401611bf5565b60008060408385031215611e7957600080fd5b82359150602083013567ffffffffffffffff811115611e9757600080fd5b611e0f85828601611d49565b60005b83811015611ebe578181015183820152602001611ea6565b50506000910152565b60008151808452611edf816020860160208601611ea3565b601f01601f19169290920160200192915050565b602081526000611bc66020830184611ec7565b634e487b7160e01b600052602160045260246000fd5b60048110611f3a57634e487b7160e01b600052602160045260246000fd5b9052565b8681526001600160a01b0386811660208301528516604082015260c060608201819052600090611f7090830186611ec7565b9050611f7f6080830185611f1c565b8260a0830152979650505050505050565b60208152815160208201526000602083015160018060a01b0380821660408501528060408601511660608501525050606083015160c06080840152611fd860e0840182611ec7565b90506080840151611fec60a0850182611f1c565b5060a084015160c08401528091505092915050565b60208082526016908201527514d95c9d9a58d948191bd95cc81b9bdd08195e1a5cdd60521b604082015260600190565b6020808252601190820152702737ba1039b2b93b34b1b29037bbb732b960791b604082015260600190565b634e487b7160e01b600052601160045260246000fd5b6000600182016120845761208461205c565b5060010190565b600181811c9082168061209f57607f821691505b60208210810361055f57634e487b7160e01b600052602260045260246000fd5b601f821115611781576000816000526020600020601f850160051c810160208610156120e85750805b601f850160051c820191505b81811015612107578281556001016120f4565b505050505050565b815167ffffffffffffffff81111561212957612129611cbd565b61213d81612137845461208b565b846120bf565b602080601f831160018114612172576000841561215a5750858301515b600019600386901b1c1916600185901b178555612107565b600085815260208120601f198616915b828110156121a157888601518255948401946001909101908401612182565b50858210156121bf5787850151600019600388901b60f8161c191681555b5050505050600190811b01905550565b6040815260006121e26040830185611ec7565b90508260208301529392505050565b634e487b7160e01b600052603260045260246000fd5b818103818111156109b1576109b161205c565b634e487b7160e01b600052603160045260246000fd5b60006020828403121561224257600080fd5b5051919050565b6000825161225b818460208701611ea3565b919091019291505056fe360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbca2646970667358221220d9549bd0f79d88e74336e4e604a702e758534a1e4549564f637caba37d89fc3464736f6c63430008160033", + "linkReferences": {}, + "deployedLinkReferences": {} +} diff --git a/packages/sdk/src/ensemble.ts b/packages/sdk/src/ensemble.ts index 9fd960d..57d4d94 100644 --- a/packages/sdk/src/ensemble.ts +++ b/packages/sdk/src/ensemble.ts @@ -5,10 +5,13 @@ import { AgentRecord, AgentMetadata, RegisterAgentParams, + RegisterServiceParams, + UpdateServiceParams, EnsembleConfig, TaskData, TaskCreationParams, - Service, + ServiceRecord, + ServiceStatus, } from "./types"; import { TaskService } from "./services/TaskService"; import { AgentService } from "./services/AgentService"; @@ -38,6 +41,13 @@ export class Ensemble { return this.agentService; } + /** + * Get the service registry service instance + */ + get services(): ServiceRegistryService { + return this.serviceRegistryService; + } + /** * Set the signer for write operations * @param {ethers.Signer} signer - The signer to use for write operations @@ -82,7 +92,7 @@ export class Ensemble { provider ); - const serviceRegistryService = new ServiceRegistryService(serviceRegistry, signer); + const serviceRegistryService = new ServiceRegistryService(serviceRegistry, signer, ipfsSDK); const agentService = new AgentService(agentRegistry, config.subgraphUrl, signer, ipfsSDK); const taskService = new TaskService(taskRegistry, agentService); @@ -221,7 +231,7 @@ export class Ensemble { /** * Gets data for a specific agent. - * @param {string} agentAddress - The address of the agent. + * @param {string} agentId - The ID of the agent. * @returns {Promise} A promise that resolves to the agent record. */ async getAgentRecord(agentId: string): Promise { @@ -265,24 +275,78 @@ export class Ensemble { /** * Registers a new service. - * @param {Service} service - The service to register. - * @returns {Promise} A promise that resolves to a boolean indicating if the service is registered. + * @param {RegisterServiceParams} params - The service registration parameters. + * @returns {Promise} A promise that resolves to the registered service. * @requires signer */ - async registerService(service: Service): Promise { + async registerService(params: RegisterServiceParams): Promise { this.requireSigner(); - return this.serviceRegistryService.registerService(service); + return this.serviceRegistryService.registerService(params); } /** * Gets a service by name. * @param {string} name - The name of the service. - * @returns {Promise} A promise that resolves to the service. + * @returns {Promise} A promise that resolves to the service. */ - async getService(name: string): Promise { + async getService(name: string): Promise { return this.serviceRegistryService.getService(name); } + /** + * Gets a service by its ID. + * @param {string} serviceId - The ID of the service. + * @returns {Promise} A promise that resolves to the service. + */ + async getServiceById(serviceId: string): Promise { + return this.serviceRegistryService.getServiceById(serviceId); + } + + /** + * Updates an existing service. + * @param {string} serviceId - The ID of the service to update. + * @param {UpdateServiceParams} updates - The updates to apply to the service. + * @returns {Promise} A promise that resolves to the updated service. + * @requires signer + */ + async updateService(serviceId: string, updates: UpdateServiceParams): Promise { + this.requireSigner(); + return this.serviceRegistryService.updateService(serviceId, updates); + } + + /** + * Deletes a service (soft delete - changes status to 'deleted'). + * @param {string} serviceId - The ID of the service to delete. + * @returns {Promise} A promise that resolves to true if successful. + * @requires signer + */ + async deleteService(serviceId: string): Promise { + this.requireSigner(); + return this.serviceRegistryService.deleteService(serviceId); + } + + /** + * Lists services with optional filtering and pagination. + * @param {object} options - Filter and pagination options. + * @param {string} options.owner - Filter by service owner address. + * @param {string} options.agentAddress - Filter by assigned agent address. + * @param {string} options.category - Filter by service category. + * @param {ServiceStatus[]} options.status - Filter by service status. + * @param {number} options.limit - Maximum number of results to return. + * @param {number} options.offset - Number of results to skip. + * @returns {Promise} A promise that resolves to an array of services. + */ + async listServices(options: { + owner?: string; + agentAddress?: string; + category?: string; + status?: ServiceStatus[]; + limit?: number; + offset?: number; + } = {}): Promise { + return this.serviceRegistryService.listServices(options); + } + /** * Add a proposal for an agent. * @param {string} agentAddress The address of the agent. diff --git a/packages/sdk/src/errors.ts b/packages/sdk/src/errors.ts index 31a1c5e..e0cc5fa 100644 --- a/packages/sdk/src/errors.ts +++ b/packages/sdk/src/errors.ts @@ -31,4 +31,40 @@ export class ProposalNotFoundError extends Error { super(`Proposal "${proposalId}" not found.`); this.name = "ProposalNotFoundError"; } +} + +// Service V2 Error Types +export class ServiceNotFoundError extends Error { + constructor(serviceId: string) { + super(`Service "${serviceId}" not found.`); + this.name = "ServiceNotFoundError"; + } +} + +export class ServiceOwnershipError extends Error { + constructor(serviceId: string, currentOwner: string, attemptedBy: string) { + super(`Access denied: Service "${serviceId}" is owned by "${currentOwner}", but operation was attempted by "${attemptedBy}".`); + this.name = "ServiceOwnershipError"; + } +} + +export class ServiceStatusError extends Error { + constructor(serviceId: string, currentStatus: string, requiredStatus: string) { + super(`Invalid service status: Service "${serviceId}" is "${currentStatus}" but operation requires "${requiredStatus}".`); + this.name = "ServiceStatusError"; + } +} + +export class ServiceValidationError extends Error { + constructor(message: string, public readonly validationErrors?: any) { + super(`Service validation failed: ${message}`); + this.name = "ServiceValidationError"; + } +} + +export class ServiceAgentAssignmentError extends Error { + constructor(serviceId: string, agentAddress: string, reason: string) { + super(`Cannot assign agent "${agentAddress}" to service "${serviceId}": ${reason}`); + this.name = "ServiceAgentAssignmentError"; + } } \ No newline at end of file diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 02775df..ad9ccde 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -5,7 +5,10 @@ import { ContractService } from "./services/ContractService" import { ServiceRegistryService } from "./services/ServiceRegistryService" // Export all types and interfaces -export * from "./types" +export * from "./types"; + +// Export base schemas +export * from "./schemas/base.schemas"; // Export validation functions export { @@ -16,7 +19,28 @@ export { parseAgentRecord, parseRegisterParams, parseUpdateParams -} from "./schemas/agent.schemas" +} from "./schemas/agent.schemas"; + +// Export service validation functions +export { + validateServiceRecord, + validateService, // deprecated alias + validateRegisterServiceParams, + validateUpdateServiceParams, + validateServiceOnChain, + validateServiceMetadata, + parseServiceRecord, + parseService, // deprecated alias + parseRegisterServiceParams, + parseUpdateServiceParams, + isServiceRecord, + isService, // deprecated alias + isRegisterServiceParams, + isUpdateServiceParams, + isServiceOnChain, + isServiceMetadata, + formatServiceValidationError +} from "./schemas/service.schemas" export { Ensemble, diff --git a/packages/sdk/src/schemas/agent.schemas.ts b/packages/sdk/src/schemas/agent.schemas.ts index 004961e..f3fddc8 100644 --- a/packages/sdk/src/schemas/agent.schemas.ts +++ b/packages/sdk/src/schemas/agent.schemas.ts @@ -1,13 +1,13 @@ import { z } from 'zod'; +import { + ethereumAddressRegex, + BigNumberishSchema, + EthereumAddressSchema, + URLSchema +} from './base.schemas'; -// ============================================================================ -// Base Schemas -// ============================================================================ - -/** - * Ethereum address validation regex - */ -const ethereumAddressRegex = /^0x[a-fA-F0-9]{40}$/; +// Re-export all Service schemas from service.schemas.ts +export * from './service.schemas'; /** * Schema for agent social media links @@ -84,15 +84,6 @@ export const FlexibleCommunicationParamsSchema = z.union([ // Agent Schemas // ============================================================================ -/** - * Schema for BigNumberish type (ethers.js compatible) - */ -export const BigNumberishSchema = z.union([ - z.bigint(), - z.string(), - z.number() -]); - /** * Schema for agent status */ diff --git a/packages/sdk/src/schemas/base.schemas.ts b/packages/sdk/src/schemas/base.schemas.ts new file mode 100644 index 0000000..5fa4a82 --- /dev/null +++ b/packages/sdk/src/schemas/base.schemas.ts @@ -0,0 +1,101 @@ +import { z } from 'zod'; + +// ============================================================================ +// Base Constants and Regex Patterns +// ============================================================================ + +/** + * Ethereum address validation regex + * Matches valid Ethereum addresses (0x followed by 40 hexadecimal characters) + */ +export const ethereumAddressRegex = /^0x[a-fA-F0-9]{40}$/; + +// ============================================================================ +// Base Schema Types +// ============================================================================ + +/** + * Schema for BigNumberish values (compatible with ethers.js) + * Accepts: number, string, bigint + */ +export const BigNumberishSchema = z.union([ + z.bigint(), + z.string(), + z.number() +]); + +/** + * Schema for JSON/object values + * Recursive schema for flexible key-value pairs and arrays + */ +export const JsonSchema: z.ZodType = z.lazy(() => + z.union([ + z.string(), + z.number(), + z.boolean(), + z.null(), + z.record(z.string(), JsonSchema), + z.array(JsonSchema) + ]) +); + +// ============================================================================ +// Common Utility Schemas +// ============================================================================ + +/** + * Schema for Ethereum addresses with validation + */ +export const EthereumAddressSchema = z.string().regex( + ethereumAddressRegex, + 'Invalid Ethereum address format' +); + +/** + * Schema for optional Ethereum addresses + */ +export const OptionalEthereumAddressSchema = EthereumAddressSchema.optional(); + +/** + * Schema for UUID strings + */ +export const UUIDSchema = z.string().uuid({ + message: 'Must be a valid UUID' +}); + +/** + * Schema for optional UUID strings + */ +export const OptionalUUIDSchema = UUIDSchema.optional(); + +/** + * Schema for URL strings + */ +export const URLSchema = z.string().url({ + message: 'Must be a valid URL' +}); + +/** + * Schema for optional URL strings + */ +export const OptionalURLSchema = URLSchema.optional(); + +/** + * Schema for datetime strings + */ +export const DateTimeSchema = z.string().datetime({ + message: 'Invalid datetime format' +}); + +/** + * Schema for optional datetime strings + */ +export const OptionalDateTimeSchema = DateTimeSchema.optional(); + +// ============================================================================ +// Type Exports +// ============================================================================ + +export type BigNumberish = z.infer; +export type JsonValue = z.infer; +export type EthereumAddress = z.infer; \ No newline at end of file diff --git a/packages/sdk/src/schemas/service.schemas.ts b/packages/sdk/src/schemas/service.schemas.ts new file mode 100644 index 0000000..65554a6 --- /dev/null +++ b/packages/sdk/src/schemas/service.schemas.ts @@ -0,0 +1,396 @@ +import { z } from 'zod'; +import { + ethereumAddressRegex, + BigNumberishSchema, + JsonSchema, + EthereumAddressSchema, + UUIDSchema, + URLSchema, + OptionalDateTimeSchema +} from './base.schemas'; + +// ============================================================================ +// Service Schemas +// ============================================================================ + +/** + * Schema for service categories + * Defines the types of services available in the ecosystem + */ +export const ServiceCategorySchema = z.enum([ + 'data', // Data processing and analytics + 'research', // Research and analysis + 'defi', // DeFi services + 'social', // Social media integration + 'security', // Security services + 'vibes', // Vibes services + 'other' // Other services +]); + +/** + * Schema for service HTTP methods + */ +export const ServiceMethodSchema = z.enum([ + 'HTTP_GET', + 'HTTP_POST', + 'HTTP_PUT', + 'HTTP_DELETE' +]); + +/** + * Schema for service status (on-chain) + * Represents the lifecycle state of the service + */ +export const ServiceStatusSchema = z.enum(['draft', 'published', 'archived', 'deleted']); + +/** + * Schema for service pricing models + */ +export const ServicePricingModelSchema = z.enum(['per_call', 'subscription', 'tiered', 'free']); + +/** + * Schema for service pricing + */ +export const ServicePricingSchema = z.object({ + model: ServicePricingModelSchema, + price: BigNumberishSchema.optional(), // Optional for free services + tokenAddress: z.string().regex(ethereumAddressRegex).optional(), + freeQuota: z.number().int().min(0).optional() // Free calls before charging +}); + +/** + * Schema for operational status (stored off-chain) + */ +export const ServiceOperationalStatusSchema = z.enum(['healthy', 'degraded', 'unhealthy', 'maintenance']); + +/** + * Schema for on-chain service data + * Minimal data stored on blockchain for gas efficiency + */ +export const ServiceOnChainSchema = z.object({ + id: z.string().describe('Auto-incremented service ID from blockchain'), + owner: EthereumAddressSchema, + agentAddress: z.string().regex(ethereumAddressRegex, 'Invalid agent address format').optional(), + serviceUri: z.string().describe('IPFS URI for service metadata'), + status: ServiceStatusSchema, + version: z.number().int().min(0).describe('Version for cache invalidation') +}); + +/** + * Schema for off-chain service metadata (stored in IPFS) + */ +export const ServiceMetadataSchema = z.object({ + // Descriptive information + name: z.string().min(1, 'Service name is required').max(100, 'Service name too long'), + description: z.string().min(1, 'Service description is required').max(500, 'Service description too long'), + category: ServiceCategorySchema, + + // Technical specification + endpointSchema: URLSchema, + method: ServiceMethodSchema, + parametersSchema: z.record(z.string(), JsonSchema).describe('Input parameters JSON schema'), + resultSchema: z.record(z.string(), JsonSchema).describe('Expected output JSON schema'), + + // Service characteristics + tags: z.array(z.string().min(1).max(50)).max(20).optional().describe('Service tags for discovery and categorization'), + + // Business and operational + pricing: ServicePricingSchema.optional().describe('Service pricing configuration'), + + // Timestamps (stored off-chain for gas efficiency) + createdAt: OptionalDateTimeSchema.describe('Service creation timestamp'), + updatedAt: OptionalDateTimeSchema.describe('Service last update timestamp'), + + // Operational status (updated by monitoring) + operational: z.object({ + status: ServiceOperationalStatusSchema, + health: z.number().min(0).max(100).optional().describe('Health percentage'), + lastCheck: OptionalDateTimeSchema, + uptime: z.number().optional().describe('Uptime percentage'), + responseTime: z.number().optional().describe('Average response time in ms'), + errorRate: z.number().optional().describe('Error rate percentage') + }).optional() +}).refine( + // Validate tag uniqueness + (data) => { + if (!data.tags) return true; + const uniqueTags = new Set(data.tags); + return uniqueTags.size === data.tags.length; + }, + { + message: 'Service tags must be unique', + path: ['tags'] + } +); + +/** + * Complete service record combining on-chain and off-chain data + * This is the primary interface that SDK users work with + */ +export const ServiceRecordSchema = ServiceOnChainSchema.merge( + ServiceMetadataSchema.omit({ operational: true }) +).extend({ + // Include operational status as optional at the top level + operational: ServiceMetadataSchema.shape.operational.optional() +}).refine( + // Services with 'published' status must have an agent assigned + (data) => { + if (data.status === 'published') { + return data.agentAddress && data.agentAddress !== '0x0000000000000000000000000000000000000000'; + } + return true; + }, + { + message: 'Published services must have an agent assigned', + path: ['agentAddress'] + } +); + +/** + * Legacy alias for backwards compatibility + * @deprecated Use ServiceRecordSchema instead + */ +export const ServiceSchema = ServiceRecordSchema; + +/** + * Schema for registering a new service + * Combines minimal on-chain fields with full metadata for IPFS + */ +export const RegisterServiceParamsSchema = z.object({ + // On-chain fields + name: z.string().min(1, 'Service name is required').max(100, 'Service name too long'), + agentAddress: z.string().regex(ethereumAddressRegex, 'Invalid agent address format').optional(), + + // Off-chain metadata (will be stored in IPFS) + metadata: ServiceMetadataSchema.omit({ name: true, operational: true, createdAt: true, updatedAt: true }) +}).partial({ + agentAddress: true // Optional until service is published +}); + +/** + * Schema for updating an existing service + * Allows updating on-chain fields and/or metadata + */ +export const UpdateServiceParamsSchema = z.object({ + id: z.string().describe('Service ID to update'), + + // On-chain updates (optional) + name: z.string().min(1).max(100).optional(), + agentAddress: z.string().regex(ethereumAddressRegex).optional(), + status: ServiceStatusSchema.optional(), + + // Off-chain metadata updates (optional) + metadata: ServiceMetadataSchema.omit({ name: true, operational: true, createdAt: true, updatedAt: true }).partial().optional() +}).refine( + (data) => { + // At least one field must be provided for update + return data.name || data.agentAddress || data.status || data.metadata; + }, + { + message: 'At least one field must be provided for update' + } +); + +// ============================================================================ +// Type Exports +// ============================================================================ + +export type ServiceCategory = z.infer; +export type ServiceMethod = z.infer; +export type ServiceStatus = z.infer; +export type ServiceOperationalStatus = z.infer; +export type ServicePricingModel = z.infer; +export type ServicePricing = z.infer; +export type ServiceOnChain = z.infer; +export type ServiceMetadata = z.infer; +export type ServiceRecord = z.infer; +export type RegisterServiceParams = z.infer; +export type UpdateServiceParams = z.infer; + +// Legacy type alias for backwards compatibility +export type Service = ServiceRecord; + +// ============================================================================ +// Service Validation Functions +// ============================================================================ + +/** + * Validates service record data against the ServiceRecord schema + * @param data - The data to validate + * @returns Validation result with success flag and data/error + */ +export const validateServiceRecord = (data: unknown) => { + return ServiceRecordSchema.safeParse(data); +}; + +/** + * Legacy validation function for backwards compatibility + * @deprecated Use validateServiceRecord instead + */ +export const validateService = validateServiceRecord; + +/** + * Validates service registration parameters + * @param data - The registration parameters to validate + * @returns Validation result + */ +export const validateRegisterServiceParams = (data: unknown) => { + return RegisterServiceParamsSchema.safeParse(data); +}; + +/** + * Validates service update parameters + * @param data - The update parameters to validate + * @returns Validation result + */ +export const validateUpdateServiceParams = (data: unknown) => { + return UpdateServiceParamsSchema.safeParse(data); +}; + +/** + * Validates service on-chain data + * @param data - The on-chain data to validate + * @returns Validation result + */ +export const validateServiceOnChain = (data: unknown) => { + return ServiceOnChainSchema.safeParse(data); +}; + +/** + * Validates service metadata + * @param data - The metadata to validate + * @returns Validation result + */ +export const validateServiceMetadata = (data: unknown) => { + return ServiceMetadataSchema.safeParse(data); +}; + +// ============================================================================ +// Service Parsing Functions +// ============================================================================ + +/** + * Parses and validates service record data + * @param data - The data to parse + * @throws ZodError if validation fails + */ +export const parseServiceRecord = (data: unknown): ServiceRecord => { + return ServiceRecordSchema.parse(data); +}; + +/** + * Legacy parsing function for backwards compatibility + * @deprecated Use parseServiceRecord instead + */ +export const parseService = parseServiceRecord; + +/** + * Parses and validates service registration parameters + * @param data - The registration parameters to parse + * @throws ZodError if validation fails with enhanced error messages + */ +export const parseRegisterServiceParams = (data: unknown): RegisterServiceParams => { + try { + return RegisterServiceParamsSchema.parse(data); + } catch (error) { + if (error instanceof z.ZodError) { + throw new z.ZodError([ + ...error.issues.map(issue => ({ + ...issue, + message: `Service registration: ${issue.message}` + })) + ]); + } + throw error; + } +}; + +/** + * Parses and validates service update parameters + * @param data - The update parameters to parse + * @throws ZodError if validation fails with enhanced error messages + */ +export const parseUpdateServiceParams = (data: unknown): UpdateServiceParams => { + try { + return UpdateServiceParamsSchema.parse(data); + } catch (error) { + if (error instanceof z.ZodError) { + throw new z.ZodError([ + ...error.issues.map(issue => ({ + ...issue, + message: `Service update: ${issue.message}` + })) + ]); + } + throw error; + } +}; + +// ============================================================================ +// Type Guards +// ============================================================================ + +/** + * Type guard for ServiceRecord + * @param data - Data to check + * @returns True if data matches ServiceRecord schema + */ +export const isServiceRecord = (data: unknown): data is ServiceRecord => { + return ServiceRecordSchema.safeParse(data).success; +}; + +/** + * Legacy type guard for backwards compatibility + * @deprecated Use isServiceRecord instead + */ +export const isService = isServiceRecord; + +/** + * Type guard for RegisterServiceParams + * @param data - Data to check + * @returns True if data matches RegisterServiceParams schema + */ +export const isRegisterServiceParams = (data: unknown): data is RegisterServiceParams => { + return RegisterServiceParamsSchema.safeParse(data).success; +}; + +/** + * Type guard for UpdateServiceParams + * @param data - Data to check + * @returns True if data matches UpdateServiceParams schema + */ +export const isUpdateServiceParams = (data: unknown): data is UpdateServiceParams => { + return UpdateServiceParamsSchema.safeParse(data).success; +}; + +/** + * Type guard for ServiceOnChain + * @param data - Data to check + * @returns True if data matches ServiceOnChain schema + */ +export const isServiceOnChain = (data: unknown): data is ServiceOnChain => { + return ServiceOnChainSchema.safeParse(data).success; +}; + +/** + * Type guard for ServiceMetadata + * @param data - Data to check + * @returns True if data matches ServiceMetadata schema + */ +export const isServiceMetadata = (data: unknown): data is ServiceMetadata => { + return ServiceMetadataSchema.safeParse(data).success; +}; + +/** + * Helper function to format validation errors for user display + * @param error - Zod validation error + * @returns Formatted error message + */ +export const formatServiceValidationError = (error: z.ZodError): string => { + const errorMessages = error.issues.map(issue => { + const path = issue.path.length > 0 ? `${issue.path.join('.')}: ` : ''; + return `${path}${issue.message}`; + }); + + return `Validation failed:\n${errorMessages.map(msg => ` - ${msg}`).join('\n')}`; +}; \ No newline at end of file diff --git a/packages/sdk/src/services/ServiceRegistryService.ts b/packages/sdk/src/services/ServiceRegistryService.ts index 4cda85b..fa8eb93 100644 --- a/packages/sdk/src/services/ServiceRegistryService.ts +++ b/packages/sdk/src/services/ServiceRegistryService.ts @@ -1,12 +1,55 @@ import { ethers } from "ethers"; -import { Service } from "../types"; -import { ServiceAlreadyRegisteredError } from "../errors"; +import { + ServiceRecord, + ServiceOnChain, + ServiceMetadata, + RegisterServiceParams, + UpdateServiceParams, + ServiceStatus, + TransactionResult +} from "../types"; +import { + ServiceAlreadyRegisteredError, + ServiceNotFoundError, + ServiceOwnershipError, + ServiceStatusError, + ServiceValidationError, + ServiceAgentAssignmentError +} from "../errors"; +import { + validateRegisterServiceParams, + validateUpdateServiceParams, + validateServiceRecord, + parseRegisterServiceParams, + parseUpdateServiceParams +} from "../schemas/service.schemas"; import { ServiceRegistry } from "../../typechain"; +// IPFS integration types and utilities +type PinataSDK = any; + +interface IPFSUploadResponse { + IpfsHash: string; + PinSize: number; + Timestamp: string; +} + +interface IPFSMetadata { + [key: string]: any; +} + +class IPFSError extends Error { + constructor(message: string, public readonly cause?: Error) { + super(message); + this.name = 'IPFSError'; + } +} + export class ServiceRegistryService { constructor( private readonly serviceRegistry: ServiceRegistry, - private signer?: ethers.Signer + private signer?: ethers.Signer, + private readonly ipfsSDK?: PinataSDK ) {} /** @@ -27,43 +70,877 @@ export class ServiceRegistryService { } } + // ============================================================================ + // V2 CRUD Operations + // ============================================================================ + /** - * @param service The service to register - * @returns A promise that resolves when the service is registered + * Registers a new service with comprehensive validation and metadata storage + * @param {RegisterServiceParams} params - Service registration parameters + * @returns {Promise} The registered service with generated ID and metadata + * @throws {ServiceValidationError} If validation fails + * @throws {ServiceAlreadyRegisteredError} If service name already exists */ - async registerService(service: Service): Promise { + async registerService(params: RegisterServiceParams): Promise { this.requireSigner(); + + // Validate input parameters + const validationResult = validateRegisterServiceParams(params); + if (!validationResult.success) { + throw new ServiceValidationError( + "Invalid service creation parameters", + validationResult.error.issues + ); + } + + const parsedParams = parseRegisterServiceParams(params); + try { - console.log(`Registering service: ${service.name}`); + const ownerAddress = await this.signer!.getAddress(); + + console.log(`Registering service: ${parsedParams.name}`); + // Add name and timestamps to metadata before upload + const metadataWithTimestamps = { + ...parsedParams.metadata, + name: parsedParams.name, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString() + }; + + // Upload metadata to IPFS first + let serviceUri: string; + if (this.ipfsSDK) { + const uploadResponse = await this.ipfsSDK.upload.json(metadataWithTimestamps); + serviceUri = `ipfs://${uploadResponse.IpfsHash}`; + } else { + // Fallback for testing without IPFS - store as data URI + serviceUri = `data:application/json;base64,${Buffer.from(JSON.stringify(metadataWithTimestamps)).toString('base64')}`; + } + + // Register service on blockchain with minimal data const tx = await this.serviceRegistry.registerService( - service.name, - service.category, - service.description + serviceUri, + parsedParams.agentAddress || ethers.ZeroAddress ); - console.log(`Transaction sent for service ${service.name}: ${tx.hash}`); - + const receipt = await tx.wait(); - console.log( - `Transaction confirmed for service ${service.name}: ${receipt?.hash}` + + // Extract service ID from blockchain transaction + const serviceId = this.extractServiceIdFromReceipt(receipt); + + console.log(`Service registered successfully: ${parsedParams.name} (ID: ${serviceId}, tx: ${receipt?.hash})`); + + // Create complete service record combining on-chain and off-chain data + const serviceRecord: ServiceRecord = { + // On-chain fields + id: serviceId, + owner: ownerAddress, + agentAddress: parsedParams.agentAddress || ethers.ZeroAddress, + serviceUri, + status: 'draft' as ServiceStatus, + version: 1, + + // Off-chain fields from metadata (including name and timestamps) + ...metadataWithTimestamps + }; + + // Validate complete service record + const serviceValidation = validateServiceRecord(serviceRecord); + if (!serviceValidation.success) { + throw new ServiceValidationError( + "Service validation failed after blockchain registration", + serviceValidation.error.issues + ); + } + + return serviceRecord; + } catch (error: any) { + console.error(`Error creating service ${parsedParams.name}:`, error); + if (error.reason === "Service already registered") { + throw new ServiceAlreadyRegisteredError(parsedParams.name); + } + throw error; + } + } + + /** + * Updates an existing service with ownership and validation checks + * @param {string} serviceId - The service ID to update + * @param {UpdateServiceParams} updates - Fields to update + * @returns {Promise} The updated service + * @throws {ServiceNotFoundError} If service doesn't exist + * @throws {ServiceOwnershipError} If caller doesn't own the service + * @throws {ServiceValidationError} If validation fails + */ + async updateService(serviceId: string, updates: UpdateServiceParams): Promise { + this.requireSigner(); + + // Validate update parameters + const validationResult = validateUpdateServiceParams({ ...updates, id: serviceId }); + if (!validationResult.success) { + throw new ServiceValidationError( + "Invalid service update parameters", + validationResult.error.issues ); + } + + const parsedUpdates = parseUpdateServiceParams({ ...updates, id: serviceId }); + + try { + // Get current service + const currentService = await this.getServiceById(serviceId); + + // Check ownership + await this.verifyServiceOwnership(currentService, await this.signer!.getAddress()); + + // Merge updates with current service and update timestamp + const updatedService: ServiceRecord = { + ...currentService, + ...parsedUpdates, + id: serviceId, // Ensure ID cannot be changed + owner: currentService.owner, // Ensure owner cannot be changed here + updatedAt: new Date().toISOString() // Always update timestamp on any change + }; + + // Validate updated service + const serviceValidation = validateServiceRecord(updatedService); + if (!serviceValidation.success) { + throw new ServiceValidationError( + "Updated service validation failed", + serviceValidation.error.issues + ); + } + console.log(`Updating service: ${serviceId}`); + + // Update service metadata on IPFS and blockchain + let serviceUri: string; + if (this.ipfsSDK) { + const metadataToUpload = { + name: updatedService.name, + description: updatedService.description, + category: updatedService.category, + endpointSchema: updatedService.endpointSchema, + method: updatedService.method, + parametersSchema: updatedService.parametersSchema, + resultSchema: updatedService.resultSchema, + pricing: updatedService.pricing, + tags: updatedService.tags, + createdAt: updatedService.createdAt, + updatedAt: updatedService.updatedAt + }; + const uploadResponse = await this.ipfsSDK.upload.json(metadataToUpload); + serviceUri = `ipfs://${uploadResponse.IpfsHash}`; + } else { + // Fallback for testing without IPFS + const metadata = { + name: updatedService.name, + description: updatedService.description, + category: updatedService.category, + endpointSchema: updatedService.endpointSchema, + method: updatedService.method, + parametersSchema: updatedService.parametersSchema, + resultSchema: updatedService.resultSchema, + pricing: updatedService.pricing, + tags: updatedService.tags, + createdAt: updatedService.createdAt, + updatedAt: updatedService.updatedAt + }; + serviceUri = `data:application/json;base64,${Buffer.from(JSON.stringify(metadata)).toString('base64')}`; + } + + // Update service on blockchain with new IPFS URI + const tx = await this.serviceRegistry.updateService( + serviceId, + serviceUri + ); + + const receipt = await tx.wait(); + console.log(`Service updated successfully: ${serviceId} (tx: ${receipt?.hash})`); + + return updatedService; + } catch (error: any) { + console.error(`Error updating service ${serviceId}:`, error); + throw error; + } + } + + /** + * Deletes a service (soft delete with status change to 'deleted') + * @param {string} serviceId - The service ID to delete + * @returns {Promise} Success status + * @throws {ServiceNotFoundError} If service doesn't exist + * @throws {ServiceOwnershipError} If caller doesn't own the service + * @throws {ServiceStatusError} If service cannot be deleted in current status + */ + async deleteService(serviceId: string): Promise { + this.requireSigner(); + + try { + // Get current service + const service = await this.getServiceById(serviceId); + + // Check ownership + await this.verifyServiceOwnership(service, await this.signer!.getAddress()); + + // Check if service can be deleted (not active) + if (service.status === 'published') { + throw new ServiceStatusError( + serviceId, + service.status, + 'inactive or archived' + ); + } + + console.log(`Deleting service: ${serviceId}`); + + // Update service status to deleted + await this.updateServiceStatus(serviceId, 'deleted' as ServiceStatus); + + console.log(`Service deleted successfully: ${serviceId}`); return true; } catch (error: any) { - console.error(`Error registering service ${service.name}:`, error); - if (error.reason === "Service already registered") { - throw new ServiceAlreadyRegisteredError(service.name); + console.error(`Error deleting service ${serviceId}:`, error); + throw error; + } + } + + /** + * Gets a service by its ID with full metadata + * @param {string} serviceId - The service ID + * @returns {Promise} The service data + * @throws {ServiceNotFoundError} If service doesn't exist + */ + async getServiceById(serviceId: string): Promise { + try { + // This is a placeholder implementation - will need smart contract support + // For now, falling back to the existing getService method + console.log(`Getting service by ID: ${serviceId}`); + + // In a real implementation, this would query by service ID + // For now, we'll need to implement a mapping system + throw new Error("getServiceById not yet implemented - requires smart contract V2"); + + } catch (error: any) { + console.error(`Error getting service ${serviceId}:`, error); + if (error.message.includes("not found") || error.message.includes("does not exist")) { + throw new ServiceNotFoundError(serviceId); + } + throw error; + } + } + + /** + * Lists services with optional filtering and pagination + * @param {object} options - Filter and pagination options + * @returns {Promise} Array of services + */ + async listServices(options: { + owner?: string; + agentAddress?: string; + category?: string; + status?: ServiceStatus[]; + limit?: number; + offset?: number; + } = {}): Promise { + try { + console.log("Listing services with options:", options); + + // This is a placeholder implementation - will need smart contract support + // For now, return empty array + console.warn("listServices not yet fully implemented - requires smart contract V2"); + return []; + + } catch (error: any) { + console.error("Error listing services:", error); + throw error; + } + } + + // ============================================================================ + // Ownership Management + // ============================================================================ + + /** + * Transfers service ownership to a new owner + * @param {string} serviceId - The service ID + * @param {string} newOwner - The new owner's address + * @returns {Promise} The updated service + * @throws {ServiceNotFoundError} If service doesn't exist + * @throws {ServiceOwnershipError} If caller doesn't own the service + */ + async transferServiceOwnership(serviceId: string, newOwner: string): Promise { + this.requireSigner(); + + try { + // Validate new owner address + if (!ethers.isAddress(newOwner)) { + throw new ServiceValidationError(`Invalid new owner address: ${newOwner}`); } + + // Get current service + const service = await this.getServiceById(serviceId); + + // Check current ownership + await this.verifyServiceOwnership(service, await this.signer!.getAddress()); + + console.log(`Transferring service ownership: ${serviceId} to ${newOwner}`); + + // Update service owner + const updatedService: ServiceRecord = { + ...service, + owner: newOwner, + updatedAt: new Date().toISOString() + }; + + // This will need smart contract support for ownership transfers + console.log(`Service ownership transferred: ${serviceId}`); + + return updatedService; + } catch (error: any) { + console.error(`Error transferring service ownership ${serviceId}:`, error); throw error; } } + + /** + * Gets the owner of a service + * @param {string} serviceId - The service ID + * @returns {Promise} The owner's address + */ + async getServiceOwner(serviceId: string): Promise { + const service = await this.getServiceById(serviceId); + return service.owner; + } + + // ============================================================================ + // Service Lifecycle Management + // ============================================================================ + + /** + * Updates the status of a service + * @param {string} serviceId - The service ID + * @param {ServiceStatus} status - The new status + * @returns {Promise} The updated service + * @throws {ServiceNotFoundError} If service doesn't exist + * @throws {ServiceOwnershipError} If caller doesn't own the service + */ + async updateServiceStatus(serviceId: string, status: ServiceStatus): Promise { + this.requireSigner(); + + try { + // Get current service + const service = await this.getServiceById(serviceId); + + // Check ownership + await this.verifyServiceOwnership(service, await this.signer!.getAddress()); + + // Validate status transition + this.validateStatusTransition(service.status, status); + + console.log(`Updating service status: ${serviceId} from ${service.status} to ${status}`); + + // Update service + const updatedService: ServiceRecord = { + ...service, + status, + updatedAt: new Date().toISOString() + }; + + // This will need smart contract support for status updates + console.log(`Service status updated: ${serviceId}`); + + return updatedService; + } catch (error: any) { + console.error(`Error updating service status ${serviceId}:`, error); + throw error; + } + } + + /** + * Publishes a service (changes status to 'published') + * @param {string} serviceId - The service ID + * @returns {Promise} The updated service + */ + async publishService(serviceId: string): Promise { + return this.updateServiceStatus(serviceId, 'published' as ServiceStatus); + } + + /** + * Unpublishes a service (changes status to 'archived') + * @param {string} serviceId - The service ID + * @returns {Promise} The updated service + */ + async unpublishService(serviceId: string): Promise { + return this.updateServiceStatus(serviceId, 'archived' as ServiceStatus); + } + + /** + * Archives a service (changes status to 'archived') + * @param {string} serviceId - The service ID + * @returns {Promise} The updated service + */ + async archiveService(serviceId: string): Promise { + return this.updateServiceStatus(serviceId, 'archived' as ServiceStatus); + } + + // ============================================================================ + // Agent Assignment Management + // ============================================================================ + + /** + * Assigns an agent to a service + * @param {string} serviceId - The service ID + * @param {string} agentAddress - The agent's address + * @returns {Promise} The updated service + * @throws {ServiceAgentAssignmentError} If assignment fails + */ + async assignAgentToService(serviceId: string, agentAddress: string): Promise { + this.requireSigner(); + + try { + // Validate agent address + if (!ethers.isAddress(agentAddress)) { + throw new ServiceAgentAssignmentError( + serviceId, + agentAddress, + "Invalid agent address format" + ); + } + + // Get current service + const service = await this.getServiceById(serviceId); + + // Check ownership + await this.verifyServiceOwnership(service, await this.signer!.getAddress()); + + // Check if agent is already assigned + if (service.agentAddress === agentAddress) { + throw new ServiceAgentAssignmentError( + serviceId, + agentAddress, + "Agent is already assigned to this service" + ); + } + + console.log(`Assigning agent to service: ${agentAddress} -> ${serviceId}`); + + // Call smart contract to assign agent + const tx = await this.serviceRegistry.connect(this.signer!) + .assignAgentToService(serviceId, agentAddress); + + await tx.wait(); + console.log(`Agent assigned to service: ${serviceId}`); + + // Return updated service from contract + return await this.getServiceById(serviceId); + } catch (error: any) { + console.error(`Error assigning agent to service ${serviceId}:`, error); + throw error; + } + } + + /** + * Unassigns the current agent from a service and sets the service status + * @param {string} serviceId - The service ID + * @param {ServiceStatus} newStatus - The new status for the service (draft or archived) + * @returns {Promise} The updated service + */ + async unassignAgentFromService(serviceId: string, newStatus: ServiceStatus): Promise { + this.requireSigner(); + + try { + // Validate status parameter + if (newStatus !== 'draft' && newStatus !== 'archived') { + throw new ServiceValidationError('Status must be either "draft" or "archived"'); + } + + // Get current service + const service = await this.getServiceById(serviceId); + + // Check ownership + await this.verifyServiceOwnership(service, await this.signer!.getAddress()); + + // Check if any agent is assigned + if (service.agentAddress === ethers.ZeroAddress || !service.agentAddress) { + throw new ServiceAgentAssignmentError( + serviceId, + "none", + "No agent is currently assigned to this service" + ); + } + + console.log(`Unassigning agent from service: ${service.agentAddress} <- ${serviceId} (status: ${newStatus})`); + + // Call smart contract to unassign agent and set status + const tx = await this.serviceRegistry.unassignAgentFromService( + BigInt(serviceId), + newStatus === 'draft' ? 0 : 2 // DRAFT = 0, ARCHIVED = 2 + ); + + await tx.wait(); + + console.log(`Agent unassigned from service: ${serviceId} with status: ${newStatus}`); + + // Return updated service + return this.getServiceById(serviceId); + } catch (error: any) { + console.error(`Error unassigning agent from service ${serviceId}:`, error); + throw error; + } + } + + /** + * Gets all services assigned to a specific agent via smart contract + * @param {string} agentAddress - The agent's address + * @returns {Promise} Array of services assigned to the agent + */ + async getServicesByAgent(agentAddress: string): Promise { + if (!ethers.isAddress(agentAddress)) { + throw new ServiceValidationError(`Invalid agent address: ${agentAddress}`); + } + + try { + // Call smart contract to get service IDs for agent + const serviceIds = await this.serviceRegistry.getServicesByAgent(agentAddress); + + // Fetch full service records for each ID + const services: ServiceRecord[] = []; + for (const serviceId of serviceIds) { + try { + const service = await this.getServiceById(serviceId.toString()); + services.push(service); + } catch (error) { + console.warn(`Could not fetch service ${serviceId}:`, error); + // Skip invalid services but continue processing others + } + } + + return services; + } catch (error: any) { + console.error(`Error getting services for agent ${agentAddress}:`, error); + throw error; + } + } + + /** + * Gets all agents assigned to a specific service + * @param {string} serviceId - The service ID + * @returns {Promise} Array of agent addresses assigned to the service + */ + async getAssignedAgents(serviceId: string): Promise { + try { + const service = await this.getServiceById(serviceId); + + // In ServiceRegistry V2, each service has only one agent + // Return array for consistency with interface + return service.agentAddress ? [service.agentAddress] : []; + } catch (error: any) { + console.error(`Error getting assigned agents for service ${serviceId}:`, error); + throw error; + } + } + + // ============================================================================ + // IPFS Integration Methods + // ============================================================================ + + /** + * Uploads metadata to IPFS with error handling and retry logic + * @param {IPFSMetadata} metadata - Metadata object to upload + * @returns {Promise} IPFS URI (ipfs://hash) + */ + async uploadMetadata(metadata: IPFSMetadata): Promise { + if (!this.ipfsSDK) { + throw new IPFSError('IPFS SDK not configured'); + } + + const maxRetries = 3; + let lastError: Error | null = null; + + for (let attempt = 1; attempt <= maxRetries; attempt++) { + try { + console.log(`Uploading metadata to IPFS (attempt ${attempt}/${maxRetries})`); + + const uploadResponse: IPFSUploadResponse = await this.ipfsSDK.upload.json(metadata); + + if (!uploadResponse.IpfsHash) { + throw new IPFSError('No IPFS hash returned from upload'); + } + + const ipfsUri = `ipfs://${uploadResponse.IpfsHash}`; + console.log(`Metadata uploaded to IPFS: ${ipfsUri}`); + + // Automatically pin the content + await this.pinContent(uploadResponse.IpfsHash); + + return ipfsUri; + } catch (error: any) { + lastError = error; + console.warn(`IPFS upload attempt ${attempt} failed:`, error.message); + + if (attempt < maxRetries) { + // Exponential backoff: 1s, 2s, 4s + const delay = Math.pow(2, attempt - 1) * 1000; + await new Promise(resolve => setTimeout(resolve, delay)); + } + } + } + + throw new IPFSError(`Failed to upload metadata after ${maxRetries} attempts`, lastError || undefined); + } + + /** + * Downloads metadata from IPFS with error handling + * @param {string} ipfsUri - IPFS URI (ipfs://hash) + * @returns {Promise} Downloaded metadata object + */ + async downloadMetadata(ipfsUri: string): Promise { + if (!ipfsUri.startsWith('ipfs://')) { + throw new IPFSError('Invalid IPFS URI format'); + } + + const ipfsHash = ipfsUri.replace('ipfs://', ''); + + try { + console.log(`Downloading metadata from IPFS: ${ipfsUri}`); + + if (!this.ipfsSDK) { + throw new IPFSError('IPFS SDK not configured'); + } + + // Use Pinata gateway or public gateway + const response = await fetch(`https://gateway.pinata.cloud/ipfs/${ipfsHash}`); + + if (!response.ok) { + throw new IPFSError(`HTTP ${response.status}: ${response.statusText}`); + } + + const metadata = await response.json(); + console.log(`Metadata downloaded from IPFS: ${ipfsUri}`); + + return metadata; + } catch (error: any) { + console.error(`Error downloading metadata from ${ipfsUri}:`, error); + throw new IPFSError(`Failed to download metadata from ${ipfsUri}`, error); + } + } + + /** + * Pins content to IPFS to ensure persistence + * @param {string} ipfsHash - IPFS hash to pin + * @returns {Promise} + */ + async pinContent(ipfsHash: string): Promise { + if (!this.ipfsSDK) { + console.warn('IPFS SDK not configured, skipping pin operation'); + return; + } + + try { + console.log(`Pinning content to IPFS: ${ipfsHash}`); + + // Pin the content using Pinata + await this.ipfsSDK.pinning.pinByHash({ hashToPin: ipfsHash }); + + console.log(`Content pinned successfully: ${ipfsHash}`); + } catch (error: any) { + console.warn(`Failed to pin content ${ipfsHash}:`, error.message); + // Don't throw error for pinning failures as upload was successful + } + } + + /** + * Verifies IPFS content integrity by comparing hashes + * @param {string} ipfsUri - IPFS URI to verify + * @param {string} expectedHash - Optional expected hash to compare against + * @returns {Promise} True if content is valid and accessible + */ + async verifyContentIntegrity(ipfsUri: string, expectedHash?: string): Promise { + try { + const ipfsHash = ipfsUri.replace('ipfs://', ''); + + if (expectedHash && ipfsHash !== expectedHash) { + console.warn(`Hash mismatch: expected ${expectedHash}, got ${ipfsHash}`); + return false; + } + + // Try to download content to verify it's accessible + await this.downloadMetadata(ipfsUri); + + return true; + } catch (error: any) { + console.error(`Content integrity verification failed for ${ipfsUri}:`, error); + return false; + } + } + + /** + * Creates a service with metadata uploaded to IPFS + * @param {RegisterServiceParams} serviceData - Service creation parameters + * @returns {Promise} The created service record + */ + async createServiceWithMetadata(serviceData: RegisterServiceParams): Promise { + try { + // Extract metadata for IPFS upload + const metadata: IPFSMetadata = { + name: serviceData.name, + description: serviceData.metadata?.description || '', + category: serviceData.metadata?.category || 'other', + tags: serviceData.metadata?.tags || [], + endpointSchema: serviceData.metadata?.endpointSchema || '', + method: serviceData.metadata?.method || 'HTTP_GET', + parametersSchema: serviceData.metadata?.parametersSchema || {}, + resultSchema: serviceData.metadata?.resultSchema || {}, + pricing: serviceData.metadata?.pricing, + createdAt: new Date().toISOString() + }; + + // Upload metadata to IPFS + const ipfsUri = await this.uploadMetadata(metadata); + + // Create service with the original method, which will handle IPFS upload internally + return await this.registerService(serviceData); + } catch (error: any) { + console.error('Error creating service with metadata:', error); + throw error; + } + } + + /** + * Updates service metadata on IPFS and synchronizes with blockchain + * @param {string} serviceId - Service ID to update + * @param {IPFSMetadata} metadata - New metadata object + * @returns {Promise} Updated service record + */ + async updateServiceMetadata(serviceId: string, metadata: IPFSMetadata): Promise { + this.requireSigner(); + + try { + // Get current service to verify ownership + const currentService = await this.getServiceById(serviceId); + await this.verifyServiceOwnership(currentService, await this.signer!.getAddress()); + + // Upload new metadata to IPFS + await this.uploadMetadata({ + ...metadata, + updatedAt: new Date().toISOString() + }); + + // Update service with new metadata + const updateParams = { + id: serviceId, + metadata: { + description: metadata.description, + category: metadata.category as any, + tags: metadata.tags, + endpointSchema: metadata.endpointSchema, + method: metadata.method as any, + parametersSchema: metadata.parametersSchema, + resultSchema: metadata.resultSchema, + pricing: metadata.pricing + } + }; + + return await this.updateService(serviceId, updateParams); + } catch (error: any) { + console.error(`Error updating service metadata for ${serviceId}:`, error); + throw error; + } + } + + // ============================================================================ + // Private Helper Methods + // ============================================================================ + + /** + * Extracts service ID from transaction receipt + * @param {any} receipt - Transaction receipt + * @returns {string} The service ID from the blockchain + * @throws {Error} If ServiceRegistered event not found + * @private + */ + private extractServiceIdFromReceipt(receipt: any): string { + try { + // Look for ServiceRegistered event in the logs + const serviceRegisteredTopic = this.serviceRegistry.interface.getEvent('ServiceRegistered').topicHash; + const event = receipt.logs.find((log: any) => log.topics[0] === serviceRegisteredTopic); + + if (event) { + const parsed = this.serviceRegistry.interface.parseLog(event); + if (parsed && parsed.args && parsed.args.serviceId) { + // Convert BigInt to string for consistency with UUID format + return parsed.args.serviceId.toString(); + } + } + + throw new Error('ServiceRegistered event not found in transaction receipt'); + } catch (error) { + // Fallback: extract from receipt if event parsing fails + if (receipt.events && receipt.events.length > 0) { + const serviceEvent = receipt.events.find((event: any) => event.event === 'ServiceRegistered'); + if (serviceEvent && serviceEvent.args && serviceEvent.args.serviceId) { + return serviceEvent.args.serviceId.toString(); + } + } + + console.error('Failed to extract service ID from receipt:', error); + throw new Error('Could not extract service ID from blockchain transaction'); + } + } + + /** + * Verifies that the caller owns the specified service + * @param {Service} service - The service to check + * @param {string} callerAddress - The caller's address + * @throws {ServiceOwnershipError} If ownership verification fails + * @private + */ + private async verifyServiceOwnership(service: ServiceRecord, callerAddress: string): Promise { + if (service.owner.toLowerCase() !== callerAddress.toLowerCase()) { + throw new ServiceOwnershipError(service.id, service.owner, callerAddress); + } + } + + /** + * Validates a service status transition + * @param {ServiceStatus} currentStatus - Current status + * @param {ServiceStatus} newStatus - Desired new status + * @throws {ServiceStatusError} If transition is invalid + * @private + */ + private validateStatusTransition(currentStatus: ServiceStatus, newStatus: ServiceStatus): void { + const validTransitions: Record = { + 'draft': ['published', 'deleted'], + 'published': ['archived', 'deleted'], + 'archived': ['published', 'deleted'], // Allow reactivation from archive + 'deleted': [] // No transitions from deleted state + }; + + const allowedTransitions = validTransitions[currentStatus] || []; + if (!allowedTransitions.includes(newStatus)) { + throw new ServiceStatusError( + "unknown", + currentStatus, + `Cannot transition from ${currentStatus} to ${newStatus}. Allowed: ${allowedTransitions.join(', ')}` + ); + } + } + + // ============================================================================ + // Legacy Methods (for backward compatibility) + // ============================================================================ + + /** - * Gets a service by name. - * @param {string} name - The name of the service. - * @returns {Promise} A promise that resolves to the service. + * Legacy method: Get service by name (V1 compatibility) + * @deprecated Use getServiceById() instead. V2 services don't support name-based lookup. */ - async getService(name: string): Promise { - const service = await this.serviceRegistry.getService(name); - return service; + async getService(_name: string): Promise { + throw new Error( + "getService() by name is no longer supported in V2. " + + "Service names are stored off-chain. Use getServiceById() instead." + ); } } diff --git a/packages/sdk/src/types.ts b/packages/sdk/src/types.ts index e37e5cd..3766f7d 100644 --- a/packages/sdk/src/types.ts +++ b/packages/sdk/src/types.ts @@ -68,7 +68,7 @@ export interface TaskStatusChangedEvent { status: TaskStatus; } -// Re-export types from schemas +// Re-export agent types from schemas export { AgentSocials, AgentCommunicationType, @@ -82,6 +82,22 @@ export { AgentStatus, } from './schemas/agent.schemas'; +// Re-export service types from service schemas +export { + ServiceRecord, + Service, // deprecated alias for ServiceRecord + ServiceOnChain, + ServiceMetadata, + RegisterServiceParams, + UpdateServiceParams, + ServiceCategory, + ServiceMethod, + ServiceStatus, + ServiceOperationalStatus, + ServicePricingModel, + ServicePricing, +} from './schemas/service.schemas'; + // Type alias for serialized communication parameters (JSON string) export type SerializedCommunicationParams = string; @@ -133,14 +149,6 @@ export interface Skill { level: number; } -export interface Service { - name: string; - category: string; - description: string; -} - - - export interface AgentData { name: string; agentUri: string; diff --git a/packages/sdk/test/ensemble.test.ts b/packages/sdk/test/ensemble.test.ts index 55aa89a..c139616 100644 --- a/packages/sdk/test/ensemble.test.ts +++ b/packages/sdk/test/ensemble.test.ts @@ -36,6 +36,10 @@ describe("Ensemble Unit Tests", () => { serviceRegistryService = { registerService: jest.fn(), + getServiceById: jest.fn(), + updateService: jest.fn(), + deleteService: jest.fn(), + listServices: jest.fn(), setSigner: jest.fn(), } as unknown as jest.Mocked; @@ -192,25 +196,63 @@ describe("Ensemble Unit Tests", () => { it("should successfully register a service", async () => { const service = { name: "Test Service", - category: "Utility", - description: "This is a test service.", + metadata: { + category: "other" as const, + description: "This is a test service.", + endpointSchema: "https://api.example.com/test", + method: "HTTP_POST" as const, + parametersSchema: {}, + resultSchema: {} + } + }; + + const mockServiceRecord = { + id: "test-service-id", + name: "Test Service", + owner: "0x123", + agentAddress: "0x456", + serviceUri: "ipfs://test", + status: "draft" as const, + version: 1, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + ...service.metadata }; - serviceRegistryService.registerService.mockResolvedValueOnce(true); + serviceRegistryService.registerService.mockResolvedValueOnce(mockServiceRecord); const response = await sdk.registerService(service); - expect(response).toEqual(true); + expect(response).toEqual(mockServiceRecord); }); it("should fail to register the same service twice", async () => { const service = { name: "Test Service Failed", - category: "Utility", - description: "This is a test service.", + metadata: { + category: "other" as const, + description: "This is a test service.", + endpointSchema: "https://api.example.com/test", + method: "HTTP_POST" as const, + parametersSchema: {}, + resultSchema: {} + } }; - serviceRegistryService.registerService.mockResolvedValueOnce(true); + const mockServiceRecord = { + id: "test-service-id", + name: "Test Service Failed", + owner: "0x123", + agentAddress: "0x456", + serviceUri: "ipfs://test", + status: "draft" as const, + version: 1, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + ...service.metadata + }; + + serviceRegistryService.registerService.mockResolvedValueOnce(mockServiceRecord); serviceRegistryService.registerService.mockRejectedValueOnce( new ServiceAlreadyRegisteredError(service.name) ); @@ -221,6 +263,129 @@ describe("Ensemble Unit Tests", () => { ); }); + it("should successfully get a service by ID", async () => { + const serviceId = "test-service-id"; + const mockServiceRecord = { + id: serviceId, + name: "Test Service", + owner: "0x123", + agentAddress: "0x456", + serviceUri: "ipfs://test", + status: "draft" as const, + version: 1, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + category: "other" as const, + description: "This is a test service.", + endpointSchema: "https://api.example.com/test", + method: "HTTP_POST" as const, + parametersSchema: {}, + resultSchema: {} + }; + + serviceRegistryService.getServiceById.mockResolvedValueOnce(mockServiceRecord); + const response = await sdk.getServiceById(serviceId); + + expect(response).toEqual(mockServiceRecord); + expect(serviceRegistryService.getServiceById).toHaveBeenCalledWith(serviceId); + }); + + it("should successfully update a service", async () => { + const serviceId = "test-service-id"; + const updates = { + id: serviceId, + name: "Updated Service Name", + metadata: { + description: "Updated description" + } + }; + const mockUpdatedService = { + id: serviceId, + name: "Updated Service Name", + owner: "0x123", + agentAddress: "0x456", + serviceUri: "ipfs://test", + status: "draft" as const, + version: 2, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + category: "other" as const, + description: "Updated description", + endpointSchema: "https://api.example.com/test", + method: "HTTP_POST" as const, + parametersSchema: {}, + resultSchema: {} + }; + + serviceRegistryService.updateService.mockResolvedValueOnce(mockUpdatedService); + const response = await sdk.updateService(serviceId, updates); + + expect(response).toEqual(mockUpdatedService); + expect(serviceRegistryService.updateService).toHaveBeenCalledWith(serviceId, updates); + }); + + it("should successfully delete a service", async () => { + const serviceId = "test-service-id"; + + serviceRegistryService.deleteService.mockResolvedValueOnce(true); + const response = await sdk.deleteService(serviceId); + + expect(response).toBe(true); + expect(serviceRegistryService.deleteService).toHaveBeenCalledWith(serviceId); + }); + + it("should successfully list services with filters", async () => { + const filters = { + owner: "0x123", + category: "other", + status: ["draft" as const, "published" as const], + limit: 10, + offset: 0 + }; + const mockServices = [ + { + id: "service-1", + name: "Service 1", + owner: "0x123", + agentAddress: "0x456", + serviceUri: "ipfs://test1", + status: "draft" as const, + version: 1, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + category: "other" as const, + description: "Service 1", + endpointSchema: "https://api.example.com/service1", + method: "HTTP_POST" as const, + parametersSchema: {}, + resultSchema: {} + }, + { + id: "service-2", + name: "Service 2", + owner: "0x123", + agentAddress: "0x789", + serviceUri: "ipfs://test2", + status: "published" as const, + version: 1, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + category: "other" as const, + description: "Service 2", + endpointSchema: "https://api.example.com/service2", + method: "HTTP_GET" as const, + parametersSchema: {}, + resultSchema: {} + } + ]; + + serviceRegistryService.listServices.mockResolvedValueOnce(mockServices); + const response = await sdk.listServices(filters); + + expect(response).toEqual(mockServices); + expect(serviceRegistryService.listServices).toHaveBeenCalledWith(filters); + }); + it("should not create a task without a proposal", async () => { const nonExistentProposalId = "1234"; @@ -487,8 +652,14 @@ describe("Ensemble Unit Tests", () => { it("should throw error when registerService called without signer", async () => { const service = { name: "Test Service", - category: "Utility", - description: "Test service description", + metadata: { + category: "other" as const, + description: "Test service description", + endpointSchema: "https://api.example.com/test", + method: "HTTP_POST" as const, + parametersSchema: {}, + resultSchema: {} + } }; await expect(ensemble.registerService(service)).rejects.toThrow( @@ -496,6 +667,26 @@ describe("Ensemble Unit Tests", () => { ); }); + it("should throw error when updateService called without signer", async () => { + const updates = { + id: "service-id", + name: "Updated Service Name", + metadata: { + description: "Updated description" + } + }; + + await expect(ensemble.updateService("service-id", updates)).rejects.toThrow( + "Signer required for write operations. Call setSigner() first." + ); + }); + + it("should throw error when deleteService called without signer", async () => { + await expect(ensemble.deleteService("service-id")).rejects.toThrow( + "Signer required for write operations. Call setSigner() first." + ); + }); + it("should throw error when addProposal called without signer", async () => { await expect(ensemble.addProposal( "0x123",