From 53def054a7c7f7c2d115f443a9f93c5ae62e7e24 Mon Sep 17 00:00:00 2001 From: Leon Prouger Date: Tue, 26 Aug 2025 16:09:33 +0300 Subject: [PATCH 01/22] addibg ServiceSchema --- packages/sdk/src/index.ts | 9 +- packages/sdk/src/schemas/agent.schemas.ts | 167 ++++++++++++++++++++++ packages/sdk/src/types.ts | 17 +-- 3 files changed, 184 insertions(+), 9 deletions(-) diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 02775df..066c09c 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -15,7 +15,14 @@ export { validateCommunicationParams, parseAgentRecord, parseRegisterParams, - parseUpdateParams + parseUpdateParams, + // Service validation functions + validateService, + validateCreateService, + validateUpdateService, + parseService, + parseCreateService, + isService } from "./schemas/agent.schemas" export { diff --git a/packages/sdk/src/schemas/agent.schemas.ts b/packages/sdk/src/schemas/agent.schemas.ts index 004961e..d8ea92a 100644 --- a/packages/sdk/src/schemas/agent.schemas.ts +++ b/packages/sdk/src/schemas/agent.schemas.ts @@ -312,4 +312,171 @@ export const parseCommunicationParamsFromString = (params: string | Communicatio } } return params; +}; + +// ============================================================================ +// Service Schemas +// ============================================================================ + +/** + * Schema for service categories + */ +export const ServiceCategorySchema = z.enum([ + 'ai', + 'data', + 'automation', + 'defi', + 'social', + 'analytics', + 'oracle', + 'storage', + 'compute', + 'messaging' +]); + +/** + * Schema for service methods + */ +export const ServiceMethodSchema = z.enum([ + 'HTTP_GET', + 'HTTP_POST', + 'HTTP_PUT', + 'HTTP_DELETE' +]); + +/** + * Schema for service status + */ +export const ServiceStatusSchema = z.enum(['active', 'inactive', 'deprecated']); + +/** + * 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 JSON/object values + * Using z.record for flexible key-value pairs + */ +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) + ]) +); + +/** + * Complete service schema + * Note: Secrets should NEVER be stored in the service definition. + * Use environment variables or secure key management systems instead. + */ +export const ServiceSchema = z.object({ + id: z.string().uuid({ message: 'Service ID must be a valid UUID' }), + name: z.string().min(1, 'Service name is required').max(100), + category: ServiceCategorySchema, + description: z.string().min(1, 'Service description is required').max(500), + owner: z.string().regex(ethereumAddressRegex, 'Invalid owner address'), + agentAddress: z.string().regex(ethereumAddressRegex, 'Invalid agent address'), + endpointSchema: z.string().url({ message: 'Endpoint must be a valid URL' }), + method: ServiceMethodSchema, + parametersSchema: z.record(z.string(), JsonSchema).describe('Input parameters schema'), + resultSchema: z.record(z.string(), JsonSchema).describe('Expected output schema'), + status: ServiceStatusSchema, + pricing: ServicePricingSchema.optional() +}); + +/** + * Schema for creating a new service (some fields optional or auto-generated) + */ +export const CreateServiceSchema = ServiceSchema.omit({ + id: true, + createdAt: true, + updatedAt: true +}).extend({ + id: z.string().uuid({ message: 'Service ID must be a valid UUID' }).optional() // Allow client to provide ID or auto-generate +}); + +/** + * Schema for updating a service (all fields optional except id) + */ +export const UpdateServiceSchema = ServiceSchema.partial().required({ + id: true +}); + +// ============================================================================ +// Service Type Exports +// ============================================================================ + +export type ServiceCategory = z.infer; +export type ServiceMethod = z.infer; +export type ServiceStatus = z.infer; +export type ServicePricingModel = z.infer; +export type ServicePricing = z.infer; +export type Service = z.infer; +export type CreateService = z.infer; +export type UpdateService = z.infer; + +// ============================================================================ +// Service Validation Functions +// ============================================================================ + +/** + * Validates service data + * @returns Success with parsed data or failure with errors + */ +export const validateService = (data: unknown) => { + return ServiceSchema.safeParse(data); +}; + +/** + * Validates service creation parameters + * @returns Success with parsed data or failure with errors + */ +export const validateCreateService = (data: unknown) => { + return CreateServiceSchema.safeParse(data); +}; + +/** + * Validates service update parameters + * @returns Success with parsed data or failure with errors + */ +export const validateUpdateService = (data: unknown) => { + return UpdateServiceSchema.safeParse(data); +}; + +/** + * Parses and validates service data + * @throws ZodError if validation fails + */ +export const parseService = (data: unknown): Service => { + return ServiceSchema.parse(data); +}; + +/** + * Parses and validates service creation parameters + * @throws ZodError if validation fails + */ +export const parseCreateService = (data: unknown): CreateService => { + return CreateServiceSchema.parse(data); +}; + +/** + * Type guard for Service + */ +export const isService = (data: unknown): data is Service => { + return ServiceSchema.safeParse(data).success; }; \ No newline at end of file diff --git a/packages/sdk/src/types.ts b/packages/sdk/src/types.ts index e37e5cd..641412a 100644 --- a/packages/sdk/src/types.ts +++ b/packages/sdk/src/types.ts @@ -80,6 +80,15 @@ export { RegisterAgentParams, UpdateableAgentRecord, AgentStatus, + // Service types + Service, + CreateService, + UpdateService, + ServiceCategory, + ServiceMethod, + ServiceStatus, + ServicePricingModel, + ServicePricing, } from './schemas/agent.schemas'; // Type alias for serialized communication parameters (JSON string) @@ -133,14 +142,6 @@ export interface Skill { level: number; } -export interface Service { - name: string; - category: string; - description: string; -} - - - export interface AgentData { name: string; agentUri: string; From dbb054598bba8362edcc3a885262f2e64555a4fb Mon Sep 17 00:00:00 2001 From: Leon Prouger Date: Tue, 26 Aug 2025 16:55:01 +0300 Subject: [PATCH 02/22] updating spec with service-v2 --- .taskmaster/docs/prd.txt | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/.taskmaster/docs/prd.txt b/.taskmaster/docs/prd.txt index 00293be..49a8873 100644 --- a/.taskmaster/docs/prd.txt +++ b/.taskmaster/docs/prd.txt @@ -28,11 +28,23 @@ 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**: Support comprehensive service metadata including: + - Service identity (UUID-based with on-chain hash representation) + - Ownership model (human developer as owner, agent as executor) + - Technical specifications (endpoint URLs, method types, parameter/result schemas) + - Business configurations (pricing models, rate limits) + - Version management and status tracking +- **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 +- **REQ-1.4**: Provide advanced service discovery and filtering: + - Query by owner, agent, category, status + - Filter by pricing model and availability + - Search unassigned services for marketplace discovery #### Agent Registry - **REQ-1.5**: Allow agents to self-register with metadata and capabilities From a15b374d74874a4cfeb3d176b82c5f68b8d165c4 Mon Sep 17 00:00:00 2001 From: Leon Prouger Date: Tue, 26 Aug 2025 17:05:17 +0300 Subject: [PATCH 03/22] expand tasks --- .taskmaster/tasks/tasks.json | 71 +++++++++++++++++++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/.taskmaster/tasks/tasks.json b/.taskmaster/tasks/tasks.json index ef2c5ce..05128f7 100644 --- a/.taskmaster/tasks/tasks.json +++ b/.taskmaster/tasks/tasks.json @@ -331,11 +331,80 @@ ], "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": "pending", + "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": "pending", + "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": "pending", + "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": "pending", + "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." + } + ] } ], "metadata": { "created": "2025-07-20T10:42:18.955Z", - "updated": "2025-08-22T06:26:14.340Z", + "updated": "2025-08-26T13:57:12.098Z", "description": "Tasks for master context" } } From de30b73f113abaf6ca27c02ee9ee6a7cc8a5ad88 Mon Sep 17 00:00:00 2001 From: Leon Prouger Date: Wed, 27 Aug 2025 12:28:20 +0300 Subject: [PATCH 04/22] Services SDK draft --- .taskmaster/tasks/tasks.json | 6 +- packages/sdk/src/ensemble.ts | 9 +- packages/sdk/src/errors.ts | 36 ++ packages/sdk/src/index.ts | 24 +- packages/sdk/src/schemas/agent.schemas.ts | 192 +----- packages/sdk/src/schemas/base.schemas.ts | 101 +++ packages/sdk/src/schemas/service.schemas.ts | 281 +++++++++ .../src/services/ServiceRegistryService.ts | 595 +++++++++++++++++- packages/sdk/src/types.ts | 2 +- 9 files changed, 1030 insertions(+), 216 deletions(-) create mode 100644 packages/sdk/src/schemas/base.schemas.ts create mode 100644 packages/sdk/src/schemas/service.schemas.ts diff --git a/.taskmaster/tasks/tasks.json b/.taskmaster/tasks/tasks.json index 05128f7..17cc089 100644 --- a/.taskmaster/tasks/tasks.json +++ b/.taskmaster/tasks/tasks.json @@ -352,7 +352,7 @@ "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": "pending", + "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." }, { @@ -363,7 +363,7 @@ "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": "pending", + "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." }, { @@ -404,7 +404,7 @@ ], "metadata": { "created": "2025-07-20T10:42:18.955Z", - "updated": "2025-08-26T13:57:12.098Z", + "updated": "2025-08-26T14:18:39.372Z", "description": "Tasks for master context" } } diff --git a/packages/sdk/src/ensemble.ts b/packages/sdk/src/ensemble.ts index 9fd960d..f4dda7a 100644 --- a/packages/sdk/src/ensemble.ts +++ b/packages/sdk/src/ensemble.ts @@ -5,6 +5,7 @@ import { AgentRecord, AgentMetadata, RegisterAgentParams, + RegisterServiceParams, EnsembleConfig, TaskData, TaskCreationParams, @@ -265,13 +266,13 @@ 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); } /** 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 066c09c..764a904 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 { @@ -15,15 +18,22 @@ export { validateCommunicationParams, parseAgentRecord, parseRegisterParams, - parseUpdateParams, - // Service validation functions + parseUpdateParams +} from "./schemas/agent.schemas"; + +// Export service validation functions +export { validateService, - validateCreateService, + validateRegisterServiceParams, validateUpdateService, parseService, - parseCreateService, - isService -} from "./schemas/agent.schemas" + parseRegisterServiceParams, + parseUpdateService, + isService, + isRegisterServiceParams, + isUpdateService, + 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 d8ea92a..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 */ @@ -312,171 +303,4 @@ export const parseCommunicationParamsFromString = (params: string | Communicatio } } return params; -}; - -// ============================================================================ -// Service Schemas -// ============================================================================ - -/** - * Schema for service categories - */ -export const ServiceCategorySchema = z.enum([ - 'ai', - 'data', - 'automation', - 'defi', - 'social', - 'analytics', - 'oracle', - 'storage', - 'compute', - 'messaging' -]); - -/** - * Schema for service methods - */ -export const ServiceMethodSchema = z.enum([ - 'HTTP_GET', - 'HTTP_POST', - 'HTTP_PUT', - 'HTTP_DELETE' -]); - -/** - * Schema for service status - */ -export const ServiceStatusSchema = z.enum(['active', 'inactive', 'deprecated']); - -/** - * 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 JSON/object values - * Using z.record for flexible key-value pairs - */ -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) - ]) -); - -/** - * Complete service schema - * Note: Secrets should NEVER be stored in the service definition. - * Use environment variables or secure key management systems instead. - */ -export const ServiceSchema = z.object({ - id: z.string().uuid({ message: 'Service ID must be a valid UUID' }), - name: z.string().min(1, 'Service name is required').max(100), - category: ServiceCategorySchema, - description: z.string().min(1, 'Service description is required').max(500), - owner: z.string().regex(ethereumAddressRegex, 'Invalid owner address'), - agentAddress: z.string().regex(ethereumAddressRegex, 'Invalid agent address'), - endpointSchema: z.string().url({ message: 'Endpoint must be a valid URL' }), - method: ServiceMethodSchema, - parametersSchema: z.record(z.string(), JsonSchema).describe('Input parameters schema'), - resultSchema: z.record(z.string(), JsonSchema).describe('Expected output schema'), - status: ServiceStatusSchema, - pricing: ServicePricingSchema.optional() -}); - -/** - * Schema for creating a new service (some fields optional or auto-generated) - */ -export const CreateServiceSchema = ServiceSchema.omit({ - id: true, - createdAt: true, - updatedAt: true -}).extend({ - id: z.string().uuid({ message: 'Service ID must be a valid UUID' }).optional() // Allow client to provide ID or auto-generate -}); - -/** - * Schema for updating a service (all fields optional except id) - */ -export const UpdateServiceSchema = ServiceSchema.partial().required({ - id: true -}); - -// ============================================================================ -// Service Type Exports -// ============================================================================ - -export type ServiceCategory = z.infer; -export type ServiceMethod = z.infer; -export type ServiceStatus = z.infer; -export type ServicePricingModel = z.infer; -export type ServicePricing = z.infer; -export type Service = z.infer; -export type CreateService = z.infer; -export type UpdateService = z.infer; - -// ============================================================================ -// Service Validation Functions -// ============================================================================ - -/** - * Validates service data - * @returns Success with parsed data or failure with errors - */ -export const validateService = (data: unknown) => { - return ServiceSchema.safeParse(data); -}; - -/** - * Validates service creation parameters - * @returns Success with parsed data or failure with errors - */ -export const validateCreateService = (data: unknown) => { - return CreateServiceSchema.safeParse(data); -}; - -/** - * Validates service update parameters - * @returns Success with parsed data or failure with errors - */ -export const validateUpdateService = (data: unknown) => { - return UpdateServiceSchema.safeParse(data); -}; - -/** - * Parses and validates service data - * @throws ZodError if validation fails - */ -export const parseService = (data: unknown): Service => { - return ServiceSchema.parse(data); -}; - -/** - * Parses and validates service creation parameters - * @throws ZodError if validation fails - */ -export const parseCreateService = (data: unknown): CreateService => { - return CreateServiceSchema.parse(data); -}; - -/** - * Type guard for Service - */ -export const isService = (data: unknown): data is Service => { - return ServiceSchema.safeParse(data).success; }; \ No newline at end of file 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..22f91c2 --- /dev/null +++ b/packages/sdk/src/schemas/service.schemas.ts @@ -0,0 +1,281 @@ +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([ + 'ai', // AI/ML services + 'data', // Data processing and analytics + 'automation', // Task automation + 'defi', // DeFi services + 'social', // Social media integration + 'analytics', // Analytics and reporting + 'oracle', // Blockchain oracles + 'storage', // Data storage + 'compute', // Computational services + 'messaging' // Communication services +]); + +/** + * Schema for service HTTP methods + */ +export const ServiceMethodSchema = z.enum([ + 'HTTP_GET', + 'HTTP_POST', + 'HTTP_PUT', + 'HTTP_DELETE' +]); + +/** + * Schema for service status + */ +export const ServiceStatusSchema = z.enum(['draft', 'active', 'inactive', '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 +}); + +/** + * Complete service schema with all fields + * Note: Secrets should NEVER be stored in the service definition. + * Use environment variables or secure key management systems instead. + */ +export const ServiceSchema = z.object({ + // Core identity fields (required) + id: UUIDSchema, + 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, + owner: EthereumAddressSchema, + status: ServiceStatusSchema, + + // Agent assignment (optional but required for service to go live) + agentAddress: z.string().regex(ethereumAddressRegex, 'Invalid agent address format').optional(), + + // 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 + createdAt: OptionalDateTimeSchema, + updatedAt: OptionalDateTimeSchema +}).refine( + // Services with 'active' status must have an agent assigned + (data) => { + if (data.status === 'active') { + return data.agentAddress && data.agentAddress !== '0x0000000000000000000000000000000000000000'; + } + return true; + }, + { + message: 'Active services must have an agent assigned', + path: ['agentAddress'] + } +).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'] + } +); + +/** + * Schema for creating a new service (omits generated fields) + */ +export const RegisterServiceParamsSchema = ServiceSchema.omit({ + createdAt: true, // Generated + updatedAt: true, // Generated +}).partial({ + id: true, // Optional - generated if not provided + status: true, // Defaults to 'draft' +}); + +/** + * Schema for updating an existing service + * All fields optional except ID + */ +export const UpdateServiceSchema = ServiceSchema.partial().required({ + id: true +}); + +// ============================================================================ +// Type Exports +// ============================================================================ + +export type ServiceCategory = z.infer; +export type ServiceMethod = z.infer; +export type ServiceStatus = z.infer; +export type ServicePricingModel = z.infer; +export type ServicePricing = z.infer; +export type Service = z.infer; +export type RegisterServiceParams = z.infer; +export type UpdateService = z.infer; + +// ============================================================================ +// Service Validation Functions +// ============================================================================ + +/** + * Validates service data against the Service schema + * @param data - The data to validate + * @returns Validation result with success flag and data/error + */ +export const validateService = (data: unknown) => { + return ServiceSchema.safeParse(data); +}; + +/** + * 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 validateUpdateService = (data: unknown) => { + return UpdateServiceSchema.safeParse(data); +}; + +// ============================================================================ +// Service Parsing Functions +// ============================================================================ + +/** + * Parses and validates service data + * @param data - The data to parse + * @throws ZodError if validation fails + */ +export const parseService = (data: unknown): Service => { + return ServiceSchema.parse(data); +}; + +/** + * 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 parseUpdateService = (data: unknown): UpdateService => { + try { + return UpdateServiceSchema.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 Service + * @param data - Data to check + * @returns True if data matches Service schema + */ +export const isService = (data: unknown): data is Service => { + return ServiceSchema.safeParse(data).success; +}; + +/** + * 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 UpdateService + * @param data - Data to check + * @returns True if data matches UpdateService schema + */ +export const isUpdateService = (data: unknown): data is UpdateService => { + return UpdateServiceSchema.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..f150e60 100644 --- a/packages/sdk/src/services/ServiceRegistryService.ts +++ b/packages/sdk/src/services/ServiceRegistryService.ts @@ -1,6 +1,26 @@ import { ethers } from "ethers"; -import { Service } from "../types"; -import { ServiceAlreadyRegisteredError } from "../errors"; +import { + Service, + RegisterServiceParams, + UpdateService, + ServiceStatus, + TransactionResult +} from "../types"; +import { + ServiceAlreadyRegisteredError, + ServiceNotFoundError, + ServiceOwnershipError, + ServiceStatusError, + ServiceValidationError, + ServiceAgentAssignmentError +} from "../errors"; +import { + validateRegisterServiceParams, + validateUpdateService, + validateService, + parseRegisterServiceParams, + parseUpdateService +} from "../schemas/service.schemas"; import { ServiceRegistry } from "../../typechain"; export class ServiceRegistryService { @@ -27,43 +47,584 @@ 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}`); + // Generate UUID if not provided + const serviceId = parsedParams.id || crypto.randomUUID(); + const ownerAddress = await this.signer!.getAddress(); + + // Create complete service object + const service: Service = { + ...parsedParams, + id: serviceId, + owner: ownerAddress, + agentAddress: parsedParams.agentAddress || ethers.ZeroAddress, + status: 'draft' as ServiceStatus, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString() + }; + + // Validate complete service + const serviceValidation = validateService(service); + if (!serviceValidation.success) { + throw new ServiceValidationError( + "Service validation failed after creation", + serviceValidation.error.issues + ); + } + + console.log(`Creating service: ${service.name} (ID: ${serviceId})`); + // Register service on blockchain (using existing method for now) const tx = await this.serviceRegistry.registerService( service.name, service.category, service.description ); - 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}` + console.log(`Service created successfully: ${serviceId} (tx: ${receipt?.hash})`); + + return service; + } 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 {UpdateService} 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: UpdateService): Promise { + this.requireSigner(); + + // Validate update parameters + const validationResult = validateUpdateService({ ...updates, id: serviceId }); + if (!validationResult.success) { + throw new ServiceValidationError( + "Invalid service update parameters", + validationResult.error.issues ); + } + + const parsedUpdates = parseUpdateService({ ...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 + const updatedService: Service = { + ...currentService, + ...parsedUpdates, + id: serviceId, // Ensure ID cannot be changed + owner: currentService.owner, // Ensure owner cannot be changed here + updatedAt: new Date().toISOString() + }; + + // Validate updated service + const serviceValidation = validateService(updatedService); + if (!serviceValidation.success) { + throw new ServiceValidationError( + "Updated service validation failed", + serviceValidation.error.issues + ); + } + + console.log(`Updating service: ${serviceId}`); + + // Update service on blockchain + // Note: This will need to be updated when smart contract supports V2 operations + const tx = await this.serviceRegistry.updateService( + updatedService.name, + updatedService.category, + updatedService.description + ); + + 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 === 'active') { + 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: Service = { + ...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: Service = { + ...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; + } + } + + /** + * Activates a service (changes status to 'active') + * @param {string} serviceId - The service ID + * @returns {Promise} The updated service + */ + async activateService(serviceId: string): Promise { + return this.updateServiceStatus(serviceId, 'active' as ServiceStatus); + } + + /** + * Deactivates a service (changes status to 'inactive') + * @param {string} serviceId - The service ID + * @returns {Promise} The updated service + */ + async deactivateService(serviceId: string): Promise { + return this.updateServiceStatus(serviceId, 'inactive' 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}`); + + // Update service with agent assignment + const updatedService: Service = { + ...service, + agentAddress, + updatedAt: new Date().toISOString() + }; + + // This will need smart contract support for agent assignments + console.log(`Agent assigned to service: ${serviceId}`); + + return updatedService; + } catch (error: any) { + console.error(`Error assigning agent to service ${serviceId}:`, error); + throw error; + } + } + + /** + * Unassigns the current agent from a service + * @param {string} serviceId - The service ID + * @returns {Promise} The updated service + */ + async unassignAgentFromService(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 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}`); + + // Update service to remove agent assignment + const updatedService: Service = { + ...service, + agentAddress: ethers.ZeroAddress, + updatedAt: new Date().toISOString() + }; + + // This will need smart contract support for agent unassignment + console.log(`Agent unassigned from service: ${serviceId}`); + + return updatedService; + } catch (error: any) { + console.error(`Error unassigning agent from service ${serviceId}:`, error); throw error; } } + + /** + * Gets all services assigned to a specific agent + * @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}`); + } + + return this.listServices({ agentAddress }); + } + + // ============================================================================ + // Private Helper Methods + // ============================================================================ + + /** + * 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: Service, 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': ['active', 'deleted'], + 'active': ['inactive', 'archived', 'deleted'], + 'inactive': ['active', 'archived', 'deleted'], + 'archived': ['active', '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) + // ============================================================================ + + /** + * Legacy method: Register a service (V1 compatibility) + * @deprecated Use registerService() with RegisterServiceParams instead + */ + async registerServiceLegacy(service: Service): Promise { + console.warn("registerServiceLegacy() is deprecated. Use registerService() with RegisterServiceParams instead."); + + try { + // Convert Service to RegisterServiceParams format for compatibility + const createParams: RegisterServiceParams = { + name: service.name, + description: service.description, + category: service.category, + owner: service.owner, + agentAddress: service.agentAddress, + endpointSchema: service.endpointSchema, + method: service.method, + parametersSchema: service.parametersSchema, + resultSchema: service.resultSchema, + pricing: service.pricing, + tags: service.tags + }; + + await this.registerService(createParams); + return true; + } catch (error) { + return false; + } + } + /** - * 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 */ async getService(name: string): Promise { - const service = await this.serviceRegistry.getService(name); + console.warn("getService() by name is deprecated. Use getServiceById() instead."); + + // This is a placeholder - in reality we'd need a name->ID mapping + const contractResult = await this.serviceRegistry.getService(name); + + // Convert contract result to Service type (incomplete - needs proper mapping) + const service: Service = { + id: crypto.randomUUID(), // Placeholder - should come from contract + name: contractResult.name, + category: contractResult.category as any, + description: contractResult.description, + owner: ethers.ZeroAddress, // Placeholder - should come from contract + agentAddress: ethers.ZeroAddress, // Placeholder - should come from contract + endpointSchema: "", // Placeholder - should come from metadata + method: "HTTP_POST" as any, // Placeholder - should come from metadata + parametersSchema: {}, // Placeholder - should come from metadata + resultSchema: {}, // Placeholder - should come from metadata + status: "active" as any, // Placeholder - should come from contract + pricing: undefined, // Placeholder - should come from metadata + createdAt: new Date().toISOString(), // Placeholder + updatedAt: new Date().toISOString() // Placeholder + }; + return service; } } diff --git a/packages/sdk/src/types.ts b/packages/sdk/src/types.ts index 641412a..1867a24 100644 --- a/packages/sdk/src/types.ts +++ b/packages/sdk/src/types.ts @@ -82,7 +82,7 @@ export { AgentStatus, // Service types Service, - CreateService, + RegisterServiceParams, UpdateService, ServiceCategory, ServiceMethod, From d3485a92e482cf899702807d309e0ef5d9bf835d Mon Sep 17 00:00:00 2001 From: Leon Prouger Date: Wed, 27 Aug 2025 12:55:51 +0300 Subject: [PATCH 05/22] select categories --- packages/sdk/src/schemas/service.schemas.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/sdk/src/schemas/service.schemas.ts b/packages/sdk/src/schemas/service.schemas.ts index 22f91c2..3039e30 100644 --- a/packages/sdk/src/schemas/service.schemas.ts +++ b/packages/sdk/src/schemas/service.schemas.ts @@ -18,16 +18,13 @@ import { * Defines the types of services available in the ecosystem */ export const ServiceCategorySchema = z.enum([ - 'ai', // AI/ML services 'data', // Data processing and analytics - 'automation', // Task automation + 'research', // Research and analysis 'defi', // DeFi services 'social', // Social media integration - 'analytics', // Analytics and reporting - 'oracle', // Blockchain oracles - 'storage', // Data storage - 'compute', // Computational services - 'messaging' // Communication services + 'security', // Security services + 'vibes', // Vibes services + 'other' // Other services ]); /** From 40d80fe2e3ec29c230960ca3c333efc1bc17f23c Mon Sep 17 00:00:00 2001 From: Leon Prouger Date: Wed, 27 Aug 2025 13:29:00 +0300 Subject: [PATCH 06/22] update registerService --- packages/sdk/src/schemas/service.schemas.ts | 4 +- .../src/services/ServiceRegistryService.ts | 70 ++++++++++++++----- 2 files changed, 56 insertions(+), 18 deletions(-) diff --git a/packages/sdk/src/schemas/service.schemas.ts b/packages/sdk/src/schemas/service.schemas.ts index 3039e30..72c60b5 100644 --- a/packages/sdk/src/schemas/service.schemas.ts +++ b/packages/sdk/src/schemas/service.schemas.ts @@ -115,13 +115,13 @@ export const ServiceSchema = z.object({ ); /** - * Schema for creating a new service (omits generated fields) + * Schema for registering a new service (omits generated fields) */ export const RegisterServiceParamsSchema = ServiceSchema.omit({ + id: true, // Generated on-chain createdAt: true, // Generated updatedAt: true, // Generated }).partial({ - id: true, // Optional - generated if not provided status: true, // Defaults to 'draft' }); diff --git a/packages/sdk/src/services/ServiceRegistryService.ts b/packages/sdk/src/services/ServiceRegistryService.ts index f150e60..0626009 100644 --- a/packages/sdk/src/services/ServiceRegistryService.ts +++ b/packages/sdk/src/services/ServiceRegistryService.ts @@ -73,11 +73,25 @@ export class ServiceRegistryService { const parsedParams = parseRegisterServiceParams(params); try { - // Generate UUID if not provided - const serviceId = parsedParams.id || crypto.randomUUID(); const ownerAddress = await this.signer!.getAddress(); + + console.log(`Registering service: ${parsedParams.name}`); + + // Register service on blockchain - this will return a service ID + const tx = await this.serviceRegistry.registerService( + parsedParams.name, + parsedParams.category, + parsedParams.description + ); + + const receipt = await tx.wait(); + + // Extract service ID from blockchain transaction + const serviceId = this.extractServiceIdFromReceipt(receipt); - // Create complete service object + console.log(`Service registered successfully: ${parsedParams.name} (ID: ${serviceId}, tx: ${receipt?.hash})`); + + // Create complete service object with blockchain-generated ID const service: Service = { ...parsedParams, id: serviceId, @@ -92,23 +106,11 @@ export class ServiceRegistryService { const serviceValidation = validateService(service); if (!serviceValidation.success) { throw new ServiceValidationError( - "Service validation failed after creation", + "Service validation failed after blockchain registration", serviceValidation.error.issues ); } - console.log(`Creating service: ${service.name} (ID: ${serviceId})`); - - // Register service on blockchain (using existing method for now) - const tx = await this.serviceRegistry.registerService( - service.name, - service.category, - service.description - ); - - const receipt = await tx.wait(); - console.log(`Service created successfully: ${serviceId} (tx: ${receipt?.hash})`); - return service; } catch (error: any) { console.error(`Error creating service ${parsedParams.name}:`, error); @@ -524,6 +526,42 @@ export class ServiceRegistryService { // 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 From 1efa228e782cd74812146ddf732e0a8f1c39261e Mon Sep 17 00:00:00 2001 From: Leon Prouger Date: Wed, 27 Aug 2025 13:37:26 +0300 Subject: [PATCH 07/22] update some names --- packages/sdk/src/index.ts | 6 +++--- packages/sdk/src/schemas/service.schemas.ts | 16 ++++++++-------- .../sdk/src/services/ServiceRegistryService.ts | 14 +++++++------- packages/sdk/src/types.ts | 11 +++++++---- 4 files changed, 25 insertions(+), 22 deletions(-) diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 764a904..9f4f3a7 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -25,13 +25,13 @@ export { export { validateService, validateRegisterServiceParams, - validateUpdateService, + validateUpdateServiceParams, parseService, parseRegisterServiceParams, - parseUpdateService, + parseUpdateServiceParams, isService, isRegisterServiceParams, - isUpdateService, + isUpdateServiceParams, formatServiceValidationError } from "./schemas/service.schemas" diff --git a/packages/sdk/src/schemas/service.schemas.ts b/packages/sdk/src/schemas/service.schemas.ts index 72c60b5..cb25e9e 100644 --- a/packages/sdk/src/schemas/service.schemas.ts +++ b/packages/sdk/src/schemas/service.schemas.ts @@ -129,7 +129,7 @@ export const RegisterServiceParamsSchema = ServiceSchema.omit({ * Schema for updating an existing service * All fields optional except ID */ -export const UpdateServiceSchema = ServiceSchema.partial().required({ +export const UpdateServiceParamsSchema = ServiceSchema.partial().required({ id: true }); @@ -144,7 +144,7 @@ export type ServicePricingModel = z.infer; export type ServicePricing = z.infer; export type Service = z.infer; export type RegisterServiceParams = z.infer; -export type UpdateService = z.infer; +export type UpdateServiceParams = z.infer; // ============================================================================ // Service Validation Functions @@ -173,8 +173,8 @@ export const validateRegisterServiceParams = (data: unknown) => { * @param data - The update parameters to validate * @returns Validation result */ -export const validateUpdateService = (data: unknown) => { - return UpdateServiceSchema.safeParse(data); +export const validateUpdateServiceParams = (data: unknown) => { + return UpdateServiceParamsSchema.safeParse(data); }; // ============================================================================ @@ -216,9 +216,9 @@ export const parseRegisterServiceParams = (data: unknown): RegisterServiceParams * @param data - The update parameters to parse * @throws ZodError if validation fails with enhanced error messages */ -export const parseUpdateService = (data: unknown): UpdateService => { +export const parseUpdateServiceParams = (data: unknown): UpdateServiceParams => { try { - return UpdateServiceSchema.parse(data); + return UpdateServiceParamsSchema.parse(data); } catch (error) { if (error instanceof z.ZodError) { throw new z.ZodError([ @@ -259,8 +259,8 @@ export const isRegisterServiceParams = (data: unknown): data is RegisterServiceP * @param data - Data to check * @returns True if data matches UpdateService schema */ -export const isUpdateService = (data: unknown): data is UpdateService => { - return UpdateServiceSchema.safeParse(data).success; +export const isUpdateServiceParams = (data: unknown): data is UpdateServiceParams => { + return UpdateServiceParamsSchema.safeParse(data).success; }; /** diff --git a/packages/sdk/src/services/ServiceRegistryService.ts b/packages/sdk/src/services/ServiceRegistryService.ts index 0626009..7d32a06 100644 --- a/packages/sdk/src/services/ServiceRegistryService.ts +++ b/packages/sdk/src/services/ServiceRegistryService.ts @@ -2,7 +2,7 @@ import { ethers } from "ethers"; import { Service, RegisterServiceParams, - UpdateService, + UpdateServiceParams, ServiceStatus, TransactionResult } from "../types"; @@ -16,10 +16,10 @@ import { } from "../errors"; import { validateRegisterServiceParams, - validateUpdateService, + validateUpdateServiceParams, validateService, parseRegisterServiceParams, - parseUpdateService + parseUpdateServiceParams } from "../schemas/service.schemas"; import { ServiceRegistry } from "../../typechain"; @@ -124,17 +124,17 @@ export class ServiceRegistryService { /** * Updates an existing service with ownership and validation checks * @param {string} serviceId - The service ID to update - * @param {UpdateService} updates - Fields 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: UpdateService): Promise { + async updateService(serviceId: string, updates: UpdateServiceParams): Promise { this.requireSigner(); // Validate update parameters - const validationResult = validateUpdateService({ ...updates, id: serviceId }); + const validationResult = validateUpdateServiceParams({ ...updates, id: serviceId }); if (!validationResult.success) { throw new ServiceValidationError( "Invalid service update parameters", @@ -142,7 +142,7 @@ export class ServiceRegistryService { ); } - const parsedUpdates = parseUpdateService({ ...updates, id: serviceId }); + const parsedUpdates = parseUpdateServiceParams({ ...updates, id: serviceId }); try { // Get current service diff --git a/packages/sdk/src/types.ts b/packages/sdk/src/types.ts index 1867a24..c37aa66 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, @@ -80,16 +80,19 @@ export { RegisterAgentParams, UpdateableAgentRecord, AgentStatus, - // Service types +} from './schemas/agent.schemas'; + +// Re-export service types from service schemas +export { Service, RegisterServiceParams, - UpdateService, + UpdateServiceParams, ServiceCategory, ServiceMethod, ServiceStatus, ServicePricingModel, ServicePricing, -} from './schemas/agent.schemas'; +} from './schemas/service.schemas'; // Type alias for serialized communication parameters (JSON string) export type SerializedCommunicationParams = string; From 028d4984a04f2cf4462c9bbe2d3f0df437d3f6a0 Mon Sep 17 00:00:00 2001 From: Leon Prouger Date: Wed, 27 Aug 2025 15:44:13 +0300 Subject: [PATCH 08/22] refactor service record --- packages/sdk/src/index.ts | 13 +- packages/sdk/src/schemas/service.schemas.ts | 218 ++++++++++++++---- .../src/services/ServiceRegistryService.ts | 141 ++++++----- packages/sdk/src/types.ts | 6 +- 4 files changed, 265 insertions(+), 113 deletions(-) diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 9f4f3a7..ad9ccde 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -23,15 +23,22 @@ export { // Export service validation functions export { - validateService, + validateServiceRecord, + validateService, // deprecated alias validateRegisterServiceParams, validateUpdateServiceParams, - parseService, + validateServiceOnChain, + validateServiceMetadata, + parseServiceRecord, + parseService, // deprecated alias parseRegisterServiceParams, parseUpdateServiceParams, - isService, + isServiceRecord, + isService, // deprecated alias isRegisterServiceParams, isUpdateServiceParams, + isServiceOnChain, + isServiceMetadata, formatServiceValidationError } from "./schemas/service.schemas" diff --git a/packages/sdk/src/schemas/service.schemas.ts b/packages/sdk/src/schemas/service.schemas.ts index cb25e9e..da4db72 100644 --- a/packages/sdk/src/schemas/service.schemas.ts +++ b/packages/sdk/src/schemas/service.schemas.ts @@ -38,9 +38,10 @@ export const ServiceMethodSchema = z.enum([ ]); /** - * Schema for service status + * Schema for service status (on-chain) + * Represents the lifecycle state of the service */ -export const ServiceStatusSchema = z.enum(['draft', 'active', 'inactive', 'archived', 'deleted']); +export const ServiceStatusSchema = z.enum(['draft', 'published', 'archived', 'deleted']); /** * Schema for service pricing models @@ -58,21 +59,33 @@ export const ServicePricingSchema = z.object({ }); /** - * Complete service schema with all fields - * Note: Secrets should NEVER be stored in the service definition. - * Use environment variables or secure key management systems instead. + * Schema for operational status (stored off-chain) */ -export const ServiceSchema = z.object({ - // Core identity fields (required) - id: UUIDSchema, +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'), 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, owner: EthereumAddressSchema, - status: ServiceStatusSchema, - - // Agent assignment (optional but required for service to go live) 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'), + createdAt: OptionalDateTimeSchema, + updatedAt: OptionalDateTimeSchema +}); + +/** + * Schema for off-chain service metadata (stored in IPFS) + */ +export const ServiceMetadataSchema = z.object({ + // Descriptive information + description: z.string().min(1, 'Service description is required').max(500, 'Service description too long'), + category: ServiceCategorySchema, // Technical specification endpointSchema: URLSchema, @@ -86,22 +99,16 @@ export const ServiceSchema = z.object({ // Business and operational pricing: ServicePricingSchema.optional().describe('Service pricing configuration'), - // Timestamps - createdAt: OptionalDateTimeSchema, - updatedAt: OptionalDateTimeSchema + // 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( - // Services with 'active' status must have an agent assigned - (data) => { - if (data.status === 'active') { - return data.agentAddress && data.agentAddress !== '0x0000000000000000000000000000000000000000'; - } - return true; - }, - { - message: 'Active services must have an agent assigned', - path: ['agentAddress'] - } -).refine( // Validate tag uniqueness (data) => { if (!data.tags) return true; @@ -115,23 +122,72 @@ export const ServiceSchema = z.object({ ); /** - * Schema for registering a new service (omits generated fields) + * 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 = ServiceSchema.omit({ - id: true, // Generated on-chain - createdAt: true, // Generated - updatedAt: true, // Generated +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({ operational: true }) }).partial({ - status: true, // Defaults to 'draft' + agentAddress: true // Optional until service is published }); /** * Schema for updating an existing service - * All fields optional except ID + * Allows updating on-chain fields and/or metadata */ -export const UpdateServiceParamsSchema = ServiceSchema.partial().required({ - id: true -}); +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({ operational: 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 @@ -140,25 +196,37 @@ export const UpdateServiceParamsSchema = ServiceSchema.partial().required({ 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 Service = 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 data against the Service schema + * 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 validateService = (data: unknown) => { - return ServiceSchema.safeParse(data); +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 @@ -177,19 +245,43 @@ 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 data + * Parses and validates service record data * @param data - The data to parse * @throws ZodError if validation fails */ -export const parseService = (data: unknown): Service => { - return ServiceSchema.parse(data); +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 @@ -237,14 +329,20 @@ export const parseUpdateServiceParams = (data: unknown): UpdateServiceParams => // ============================================================================ /** - * Type guard for Service + * Type guard for ServiceRecord * @param data - Data to check - * @returns True if data matches Service schema + * @returns True if data matches ServiceRecord schema */ -export const isService = (data: unknown): data is Service => { - return ServiceSchema.safeParse(data).success; +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 @@ -255,14 +353,32 @@ export const isRegisterServiceParams = (data: unknown): data is RegisterServiceP }; /** - * Type guard for UpdateService + * Type guard for UpdateServiceParams * @param data - Data to check - * @returns True if data matches UpdateService schema + * @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 diff --git a/packages/sdk/src/services/ServiceRegistryService.ts b/packages/sdk/src/services/ServiceRegistryService.ts index 7d32a06..3a1bfc9 100644 --- a/packages/sdk/src/services/ServiceRegistryService.ts +++ b/packages/sdk/src/services/ServiceRegistryService.ts @@ -1,6 +1,8 @@ import { ethers } from "ethers"; import { - Service, + ServiceRecord, + ServiceOnChain, + ServiceMetadata, RegisterServiceParams, UpdateServiceParams, ServiceStatus, @@ -17,16 +19,20 @@ import { import { validateRegisterServiceParams, validateUpdateServiceParams, - validateService, + validateServiceRecord, parseRegisterServiceParams, parseUpdateServiceParams } from "../schemas/service.schemas"; import { ServiceRegistry } from "../../typechain"; +// TODO: Add proper IPFS SDK type when available +type PinataSDK = any; + export class ServiceRegistryService { constructor( private readonly serviceRegistry: ServiceRegistry, - private signer?: ethers.Signer + private signer?: ethers.Signer, + private readonly ipfsSDK?: PinataSDK ) {} /** @@ -58,7 +64,7 @@ export class ServiceRegistryService { * @throws {ServiceValidationError} If validation fails * @throws {ServiceAlreadyRegisteredError} If service name already exists */ - async registerService(params: RegisterServiceParams): Promise { + async registerService(params: RegisterServiceParams): Promise { this.requireSigner(); // Validate input parameters @@ -77,11 +83,21 @@ export class ServiceRegistryService { console.log(`Registering service: ${parsedParams.name}`); - // Register service on blockchain - this will return a service ID + // Upload metadata to IPFS first + let serviceUri: string; + if (this.ipfsSDK) { + const uploadResponse = await this.ipfsSDK.upload.json(parsedParams.metadata); + serviceUri = `ipfs://${uploadResponse.IpfsHash}`; + } else { + // Fallback for testing without IPFS - store as data URI + serviceUri = `data:application/json;base64,${Buffer.from(JSON.stringify(parsedParams.metadata)).toString('base64')}`; + } + + // Register service on blockchain with minimal data const tx = await this.serviceRegistry.registerService( parsedParams.name, - parsedParams.category, - parsedParams.description + serviceUri, + parsedParams.agentAddress || ethers.ZeroAddress ); const receipt = await tx.wait(); @@ -91,19 +107,25 @@ export class ServiceRegistryService { console.log(`Service registered successfully: ${parsedParams.name} (ID: ${serviceId}, tx: ${receipt?.hash})`); - // Create complete service object with blockchain-generated ID - const service: Service = { - ...parsedParams, + // Create complete service record combining on-chain and off-chain data + const serviceRecord: ServiceRecord = { + // On-chain fields id: serviceId, + name: parsedParams.name, owner: ownerAddress, agentAddress: parsedParams.agentAddress || ethers.ZeroAddress, + serviceUri, status: 'draft' as ServiceStatus, + version: 1, createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString() + updatedAt: new Date().toISOString(), + + // Off-chain fields from metadata + ...parsedParams.metadata }; - // Validate complete service - const serviceValidation = validateService(service); + // Validate complete service record + const serviceValidation = validateServiceRecord(serviceRecord); if (!serviceValidation.success) { throw new ServiceValidationError( "Service validation failed after blockchain registration", @@ -111,7 +133,7 @@ export class ServiceRegistryService { ); } - return service; + return serviceRecord; } catch (error: any) { console.error(`Error creating service ${parsedParams.name}:`, error); if (error.reason === "Service already registered") { @@ -130,7 +152,7 @@ export class ServiceRegistryService { * @throws {ServiceOwnershipError} If caller doesn't own the service * @throws {ServiceValidationError} If validation fails */ - async updateService(serviceId: string, updates: UpdateServiceParams): Promise { + async updateService(serviceId: string, updates: UpdateServiceParams): Promise { this.requireSigner(); // Validate update parameters @@ -152,7 +174,7 @@ export class ServiceRegistryService { await this.verifyServiceOwnership(currentService, await this.signer!.getAddress()); // Merge updates with current service - const updatedService: Service = { + const updatedService: ServiceRecord = { ...currentService, ...parsedUpdates, id: serviceId, // Ensure ID cannot be changed @@ -161,7 +183,7 @@ export class ServiceRegistryService { }; // Validate updated service - const serviceValidation = validateService(updatedService); + const serviceValidation = validateServiceRecord(updatedService); if (!serviceValidation.success) { throw new ServiceValidationError( "Updated service validation failed", @@ -208,7 +230,7 @@ export class ServiceRegistryService { await this.verifyServiceOwnership(service, await this.signer!.getAddress()); // Check if service can be deleted (not active) - if (service.status === 'active') { + if (service.status === 'published') { throw new ServiceStatusError( serviceId, service.status, @@ -235,7 +257,7 @@ export class ServiceRegistryService { * @returns {Promise} The service data * @throws {ServiceNotFoundError} If service doesn't exist */ - async getServiceById(serviceId: string): Promise { + 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 @@ -266,7 +288,7 @@ export class ServiceRegistryService { status?: ServiceStatus[]; limit?: number; offset?: number; - } = {}): Promise { + } = {}): Promise { try { console.log("Listing services with options:", options); @@ -293,7 +315,7 @@ export class ServiceRegistryService { * @throws {ServiceNotFoundError} If service doesn't exist * @throws {ServiceOwnershipError} If caller doesn't own the service */ - async transferServiceOwnership(serviceId: string, newOwner: string): Promise { + async transferServiceOwnership(serviceId: string, newOwner: string): Promise { this.requireSigner(); try { @@ -311,7 +333,7 @@ export class ServiceRegistryService { console.log(`Transferring service ownership: ${serviceId} to ${newOwner}`); // Update service owner - const updatedService: Service = { + const updatedService: ServiceRecord = { ...service, owner: newOwner, updatedAt: new Date().toISOString() @@ -349,7 +371,7 @@ export class ServiceRegistryService { * @throws {ServiceNotFoundError} If service doesn't exist * @throws {ServiceOwnershipError} If caller doesn't own the service */ - async updateServiceStatus(serviceId: string, status: ServiceStatus): Promise { + async updateServiceStatus(serviceId: string, status: ServiceStatus): Promise { this.requireSigner(); try { @@ -365,7 +387,7 @@ export class ServiceRegistryService { console.log(`Updating service status: ${serviceId} from ${service.status} to ${status}`); // Update service - const updatedService: Service = { + const updatedService: ServiceRecord = { ...service, status, updatedAt: new Date().toISOString() @@ -382,12 +404,12 @@ export class ServiceRegistryService { } /** - * Activates a service (changes status to 'active') + * Activates a service (changes status to 'published') * @param {string} serviceId - The service ID * @returns {Promise} The updated service */ - async activateService(serviceId: string): Promise { - return this.updateServiceStatus(serviceId, 'active' as ServiceStatus); + async activateService(serviceId: string): Promise { + return this.updateServiceStatus(serviceId, 'published' as ServiceStatus); } /** @@ -395,7 +417,7 @@ export class ServiceRegistryService { * @param {string} serviceId - The service ID * @returns {Promise} The updated service */ - async deactivateService(serviceId: string): Promise { + async deactivateService(serviceId: string): Promise { return this.updateServiceStatus(serviceId, 'inactive' as ServiceStatus); } @@ -404,7 +426,7 @@ export class ServiceRegistryService { * @param {string} serviceId - The service ID * @returns {Promise} The updated service */ - async archiveService(serviceId: string): Promise { + async archiveService(serviceId: string): Promise { return this.updateServiceStatus(serviceId, 'archived' as ServiceStatus); } @@ -419,7 +441,7 @@ export class ServiceRegistryService { * @returns {Promise} The updated service * @throws {ServiceAgentAssignmentError} If assignment fails */ - async assignAgentToService(serviceId: string, agentAddress: string): Promise { + async assignAgentToService(serviceId: string, agentAddress: string): Promise { this.requireSigner(); try { @@ -450,7 +472,7 @@ export class ServiceRegistryService { console.log(`Assigning agent to service: ${agentAddress} -> ${serviceId}`); // Update service with agent assignment - const updatedService: Service = { + const updatedService: ServiceRecord = { ...service, agentAddress, updatedAt: new Date().toISOString() @@ -471,7 +493,7 @@ export class ServiceRegistryService { * @param {string} serviceId - The service ID * @returns {Promise} The updated service */ - async unassignAgentFromService(serviceId: string): Promise { + async unassignAgentFromService(serviceId: string): Promise { this.requireSigner(); try { @@ -493,7 +515,7 @@ export class ServiceRegistryService { console.log(`Unassigning agent from service: ${service.agentAddress} <- ${serviceId}`); // Update service to remove agent assignment - const updatedService: Service = { + const updatedService: ServiceRecord = { ...service, agentAddress: ethers.ZeroAddress, updatedAt: new Date().toISOString() @@ -514,7 +536,7 @@ export class ServiceRegistryService { * @param {string} agentAddress - The agent's address * @returns {Promise} Array of services assigned to the agent */ - async getServicesByAgent(agentAddress: string): Promise { + async getServicesByAgent(agentAddress: string): Promise { if (!ethers.isAddress(agentAddress)) { throw new ServiceValidationError(`Invalid agent address: ${agentAddress}`); } @@ -569,7 +591,7 @@ export class ServiceRegistryService { * @throws {ServiceOwnershipError} If ownership verification fails * @private */ - private async verifyServiceOwnership(service: Service, callerAddress: string): Promise { + private async verifyServiceOwnership(service: ServiceRecord, callerAddress: string): Promise { if (service.owner.toLowerCase() !== callerAddress.toLowerCase()) { throw new ServiceOwnershipError(service.id, service.owner, callerAddress); } @@ -584,10 +606,9 @@ export class ServiceRegistryService { */ private validateStatusTransition(currentStatus: ServiceStatus, newStatus: ServiceStatus): void { const validTransitions: Record = { - 'draft': ['active', 'deleted'], - 'active': ['inactive', 'archived', 'deleted'], - 'inactive': ['active', 'archived', 'deleted'], - 'archived': ['active', 'deleted'], // Allow reactivation from archive + 'draft': ['published', 'deleted'], + 'published': ['archived', 'deleted'], + 'archived': ['published', 'deleted'], // Allow reactivation from archive 'deleted': [] // No transitions from deleted state }; @@ -609,23 +630,24 @@ export class ServiceRegistryService { * Legacy method: Register a service (V1 compatibility) * @deprecated Use registerService() with RegisterServiceParams instead */ - async registerServiceLegacy(service: Service): Promise { + async registerServiceLegacy(service: ServiceRecord): Promise { console.warn("registerServiceLegacy() is deprecated. Use registerService() with RegisterServiceParams instead."); try { - // Convert Service to RegisterServiceParams format for compatibility + // Convert ServiceRecord to RegisterServiceParams format for compatibility const createParams: RegisterServiceParams = { name: service.name, - description: service.description, - category: service.category, - owner: service.owner, agentAddress: service.agentAddress, - endpointSchema: service.endpointSchema, - method: service.method, - parametersSchema: service.parametersSchema, - resultSchema: service.resultSchema, - pricing: service.pricing, - tags: service.tags + metadata: { + description: service.description, + category: service.category, + endpointSchema: service.endpointSchema, + method: service.method, + parametersSchema: service.parametersSchema, + resultSchema: service.resultSchema, + pricing: service.pricing, + tags: service.tags + } }; await this.registerService(createParams); @@ -639,28 +661,31 @@ export class ServiceRegistryService { * Legacy method: Get service by name (V1 compatibility) * @deprecated Use getServiceById() instead */ - async getService(name: string): Promise { + async getService(name: string): Promise { console.warn("getService() by name is deprecated. Use getServiceById() instead."); // This is a placeholder - in reality we'd need a name->ID mapping const contractResult = await this.serviceRegistry.getService(name); - // Convert contract result to Service type (incomplete - needs proper mapping) - const service: Service = { + // Convert contract result to ServiceRecord type (incomplete - needs proper mapping) + const service: ServiceRecord = { id: crypto.randomUUID(), // Placeholder - should come from contract name: contractResult.name, - category: contractResult.category as any, - description: contractResult.description, owner: ethers.ZeroAddress, // Placeholder - should come from contract agentAddress: ethers.ZeroAddress, // Placeholder - should come from contract + serviceUri: "data://placeholder", // Placeholder - should come from contract + status: "draft" as any, // Placeholder - should come from contract + version: 1, // Placeholder - should come from contract + createdAt: new Date().toISOString(), // Placeholder + updatedAt: new Date().toISOString(), // Placeholder + // Off-chain metadata fields + description: contractResult.description, + category: contractResult.category as any, endpointSchema: "", // Placeholder - should come from metadata method: "HTTP_POST" as any, // Placeholder - should come from metadata parametersSchema: {}, // Placeholder - should come from metadata resultSchema: {}, // Placeholder - should come from metadata - status: "active" as any, // Placeholder - should come from contract pricing: undefined, // Placeholder - should come from metadata - createdAt: new Date().toISOString(), // Placeholder - updatedAt: new Date().toISOString() // Placeholder }; return service; diff --git a/packages/sdk/src/types.ts b/packages/sdk/src/types.ts index c37aa66..3766f7d 100644 --- a/packages/sdk/src/types.ts +++ b/packages/sdk/src/types.ts @@ -84,12 +84,16 @@ export { // Re-export service types from service schemas export { - Service, + ServiceRecord, + Service, // deprecated alias for ServiceRecord + ServiceOnChain, + ServiceMetadata, RegisterServiceParams, UpdateServiceParams, ServiceCategory, ServiceMethod, ServiceStatus, + ServiceOperationalStatus, ServicePricingModel, ServicePricing, } from './schemas/service.schemas'; From 5c70d29735d29c6e6ad949f32c1bc633d368c963 Mon Sep 17 00:00:00 2001 From: Leon Prouger Date: Wed, 27 Aug 2025 15:50:20 +0300 Subject: [PATCH 09/22] update PRD --- .taskmaster/docs/prd.txt | 89 ++++++++++++++++++++++++++++++++++++---- CLAUDE.md | 9 +++- 2 files changed, 89 insertions(+), 9 deletions(-) diff --git a/.taskmaster/docs/prd.txt b/.taskmaster/docs/prd.txt index 49a8873..423bccf 100644 --- a/.taskmaster/docs/prd.txt +++ b/.taskmaster/docs/prd.txt @@ -30,20 +30,32 @@ Transform AI agents from passive tools into active economic participants capable #### Service Registry V2 - **REQ-1.1**: Maintain an open catalog of available services with full lifecycle management -- **REQ-1.2**: Support comprehensive service metadata including: - - Service identity (UUID-based with on-chain hash representation) - - Ownership model (human developer as owner, agent as executor) - - Technical specifications (endpoint URLs, method types, parameter/result schemas) - - Business configurations (pricing models, rate limits) - - Version management and status tracking +- **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) + - Service name (for basic discovery) + - 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) + - Creation and update timestamps + - **Off-chain metadata** (stored in IPFS): + - Service description and detailed information + - 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: - - Query by owner, agent, category, status - - Filter by pricing model and availability + - 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 @@ -138,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/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 From 1e02a7931ca0f5c338aa0176e56318347196e8ec Mon Sep 17 00:00:00 2001 From: Leon Prouger Date: Wed, 27 Aug 2025 15:56:48 +0300 Subject: [PATCH 10/22] ServiceRecord refactor --- packages/sdk/scripts/data/servicesList.ts | 94 ++++++++++++++++++----- packages/sdk/src/ensemble.ts | 10 +-- packages/sdk/test/ensemble.test.ts | 62 ++++++++++++--- 3 files changed, 132 insertions(+), 34 deletions(-) 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/ensemble.ts b/packages/sdk/src/ensemble.ts index f4dda7a..1de4f7f 100644 --- a/packages/sdk/src/ensemble.ts +++ b/packages/sdk/src/ensemble.ts @@ -9,7 +9,7 @@ import { EnsembleConfig, TaskData, TaskCreationParams, - Service, + ServiceRecord, } from "./types"; import { TaskService } from "./services/TaskService"; import { AgentService } from "./services/AgentService"; @@ -267,10 +267,10 @@ export class Ensemble { /** * Registers a new service. * @param {RegisterServiceParams} params - The service registration parameters. - * @returns {Promise} A promise that resolves to the registered service. + * @returns {Promise} A promise that resolves to the registered service. * @requires signer */ - async registerService(params: RegisterServiceParams): Promise { + async registerService(params: RegisterServiceParams): Promise { this.requireSigner(); return this.serviceRegistryService.registerService(params); } @@ -278,9 +278,9 @@ export class Ensemble { /** * 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); } diff --git a/packages/sdk/test/ensemble.test.ts b/packages/sdk/test/ensemble.test.ts index 55aa89a..7826ed2 100644 --- a/packages/sdk/test/ensemble.test.ts +++ b/packages/sdk/test/ensemble.test.ts @@ -192,25 +192,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: {} + } }; - serviceRegistryService.registerService.mockResolvedValueOnce(true); + 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(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) ); @@ -487,8 +525,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( From af555ed65fb44f455c250c703fa3651cd07fd64e Mon Sep 17 00:00:00 2001 From: Leon Prouger Date: Wed, 27 Aug 2025 20:40:58 +0300 Subject: [PATCH 11/22] fixing tests --- packages/sdk/src/ensemble.ts | 67 ++++++++++++- packages/sdk/test/ensemble.test.ts | 147 +++++++++++++++++++++++++++++ 2 files changed, 212 insertions(+), 2 deletions(-) diff --git a/packages/sdk/src/ensemble.ts b/packages/sdk/src/ensemble.ts index 1de4f7f..57d4d94 100644 --- a/packages/sdk/src/ensemble.ts +++ b/packages/sdk/src/ensemble.ts @@ -6,10 +6,12 @@ import { AgentMetadata, RegisterAgentParams, RegisterServiceParams, + UpdateServiceParams, EnsembleConfig, TaskData, TaskCreationParams, ServiceRecord, + ServiceStatus, } from "./types"; import { TaskService } from "./services/TaskService"; import { AgentService } from "./services/AgentService"; @@ -39,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 @@ -83,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); @@ -222,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 { @@ -284,6 +293,60 @@ export class Ensemble { 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/test/ensemble.test.ts b/packages/sdk/test/ensemble.test.ts index 7826ed2..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; @@ -259,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"; @@ -540,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", From e169451643840707f43d66f811b8ea80a84b865a Mon Sep 17 00:00:00 2001 From: Leon Prouger Date: Wed, 27 Aug 2025 21:07:23 +0300 Subject: [PATCH 12/22] move timestamps off chain --- .taskmaster/docs/prd.txt | 2 +- .taskmaster/tasks/tasks.json | 16 +++++++++++++- packages/sdk/src/schemas/service.schemas.ts | 12 ++++++----- .../src/services/ServiceRegistryService.ts | 21 ++++++++++++------- 4 files changed, 36 insertions(+), 15 deletions(-) diff --git a/.taskmaster/docs/prd.txt b/.taskmaster/docs/prd.txt index 423bccf..8d29aa1 100644 --- a/.taskmaster/docs/prd.txt +++ b/.taskmaster/docs/prd.txt @@ -39,9 +39,9 @@ Transform AI agents from passive tools into active economic participants capable - Service URI (IPFS hash for metadata storage) - Status (draft, published, archived, deleted) - Version number (for cache invalidation) - - Creation and update timestamps - **Off-chain metadata** (stored in IPFS): - Service description and detailed information + - 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) diff --git a/.taskmaster/tasks/tasks.json b/.taskmaster/tasks/tasks.json index 17cc089..79867e4 100644 --- a/.taskmaster/tasks/tasks.json +++ b/.taskmaster/tasks/tasks.json @@ -400,11 +400,25 @@ "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-26T14:18:39.372Z", + "updated": "2025-08-27T17:49:46.813Z", "description": "Tasks for master context" } } diff --git a/packages/sdk/src/schemas/service.schemas.ts b/packages/sdk/src/schemas/service.schemas.ts index da4db72..97f7a12 100644 --- a/packages/sdk/src/schemas/service.schemas.ts +++ b/packages/sdk/src/schemas/service.schemas.ts @@ -74,9 +74,7 @@ export const ServiceOnChainSchema = z.object({ 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'), - createdAt: OptionalDateTimeSchema, - updatedAt: OptionalDateTimeSchema + version: z.number().int().min(0).describe('Version for cache invalidation') }); /** @@ -99,6 +97,10 @@ export const ServiceMetadataSchema = z.object({ // 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, @@ -160,7 +162,7 @@ export const RegisterServiceParamsSchema = z.object({ agentAddress: z.string().regex(ethereumAddressRegex, 'Invalid agent address format').optional(), // Off-chain metadata (will be stored in IPFS) - metadata: ServiceMetadataSchema.omit({ operational: true }) + metadata: ServiceMetadataSchema.omit({ operational: true, createdAt: true, updatedAt: true }) }).partial({ agentAddress: true // Optional until service is published }); @@ -178,7 +180,7 @@ export const UpdateServiceParamsSchema = z.object({ status: ServiceStatusSchema.optional(), // Off-chain metadata updates (optional) - metadata: ServiceMetadataSchema.omit({ operational: true }).partial().optional() + metadata: ServiceMetadataSchema.omit({ operational: true, createdAt: true, updatedAt: true }).partial().optional() }).refine( (data) => { // At least one field must be provided for update diff --git a/packages/sdk/src/services/ServiceRegistryService.ts b/packages/sdk/src/services/ServiceRegistryService.ts index 3a1bfc9..b334d9b 100644 --- a/packages/sdk/src/services/ServiceRegistryService.ts +++ b/packages/sdk/src/services/ServiceRegistryService.ts @@ -83,14 +83,21 @@ export class ServiceRegistryService { console.log(`Registering service: ${parsedParams.name}`); + // Add timestamps to metadata before upload + const metadataWithTimestamps = { + ...parsedParams.metadata, + 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(parsedParams.metadata); + 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(parsedParams.metadata)).toString('base64')}`; + serviceUri = `data:application/json;base64,${Buffer.from(JSON.stringify(metadataWithTimestamps)).toString('base64')}`; } // Register service on blockchain with minimal data @@ -117,11 +124,9 @@ export class ServiceRegistryService { serviceUri, status: 'draft' as ServiceStatus, version: 1, - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - // Off-chain fields from metadata - ...parsedParams.metadata + // Off-chain fields from metadata (including timestamps) + ...metadataWithTimestamps }; // Validate complete service record @@ -173,13 +178,13 @@ export class ServiceRegistryService { // Check ownership await this.verifyServiceOwnership(currentService, await this.signer!.getAddress()); - // Merge updates with current service + // 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() + updatedAt: new Date().toISOString() // Always update timestamp on any change }; // Validate updated service From 4ec5ca9e21da06f65fee18f07e72ef9b6ed59e66 Mon Sep 17 00:00:00 2001 From: Leon Prouger Date: Thu, 28 Aug 2025 11:51:21 +0300 Subject: [PATCH 13/22] Update the Services contract --- .taskmaster/docs/prd.txt | 4 +- .../contracts/AgentsRegistryUpgradeable.sol | 68 +- .../contracts/ServiceRegistryUpgradeable.sol | 355 ++++- packages/sdk/src/abi/ServiceRegistry.json | 1161 +++++++++++------ packages/sdk/src/schemas/service.schemas.ts | 6 +- .../src/services/ServiceRegistryService.ts | 117 +- 6 files changed, 1129 insertions(+), 582 deletions(-) diff --git a/.taskmaster/docs/prd.txt b/.taskmaster/docs/prd.txt index 8d29aa1..cfa074f 100644 --- a/.taskmaster/docs/prd.txt +++ b/.taskmaster/docs/prd.txt @@ -33,14 +33,14 @@ Transform AI agents from passive tools into active economic participants capable - **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) - - Service name (for basic discovery) - 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 description and detailed information + - 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) diff --git a/packages/contracts/contracts/AgentsRegistryUpgradeable.sol b/packages/contracts/contracts/AgentsRegistryUpgradeable.sol index eda29da..0cb03af 100644 --- a/packages/contracts/contracts/AgentsRegistryUpgradeable.sol +++ b/packages/contracts/contracts/AgentsRegistryUpgradeable.sol @@ -149,39 +149,8 @@ contract AgentsRegistryUpgradeable is Initializable, OwnableUpgradeable, UUPSUpg _createAgent(agent, name, agentUri, msg.sender, 0); } - /** - * @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); - } + // V2: registerAgentWithService removed - agents and services are managed independently + // Use registerAgent() followed by service assignment through ServiceRegistry /** * @dev Adds a new proposal for an agent. @@ -203,10 +172,11 @@ contract AgentsRegistryUpgradeable is Initializable, OwnableUpgradeable, UUPSUpg uint256 servicePrice, address tokenAddress ) public onlyAgentOwner(agent) { - require( - serviceRegistry.isServiceRegistered(serviceName), - "Service not registered" - ); + // V2: Service validation removed - services use IDs and are managed independently + // require( + // serviceRegistry.isServiceRegistered(serviceName), + // "Service not registered" + // ); _createProposal(agent, serviceName, servicePrice, tokenAddress); } @@ -392,24 +362,11 @@ contract AgentsRegistryUpgradeable is Initializable, OwnableUpgradeable, UUPSUpg nextProposalId++; } + // V2: Migration function disabled - services now use different structure 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 - ); - } + // V2: Disabled - ServiceRegistry V2 uses IDs and different parameters + // The new registerService takes (name, serviceUri, agentAddress) + // This migration logic would need complete rewrite for V2 } function _migrateAgentProposals(address agent) private { @@ -425,7 +382,8 @@ contract AgentsRegistryUpgradeable is Initializable, OwnableUpgradeable, UUPSUpg continue; } - _ensureServiceRegistered(proposal.serviceName); + // V2: Service migration disabled + // _ensureServiceRegistered(proposal.serviceName); _createProposal(agent, proposal.serviceName, proposal.price, address(0)); } diff --git a/packages/contracts/contracts/ServiceRegistryUpgradeable.sol b/packages/contracts/contracts/ServiceRegistryUpgradeable.sol index f78b718..c22e2f7 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,310 @@ 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) + { + 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 Retrieves the details of a service. - * @param name The name of the service to retrieve. - * @return The details of the service. + * @dev Updates the status of a service. + * @param serviceId The ID of the service. + * @param newStatus The new status for the service. */ - function getService(string memory name) external view returns (Service memory) { - return services[name]; + function updateServiceStatus(uint256 serviceId, ServiceStatus newStatus) + external + serviceExists(serviceId) + onlyServiceOwner(serviceId) + { + Service storage service = services[serviceId]; + ServiceStatus oldStatus = service.status; + + require(oldStatus != ServiceStatus.DELETED, "Cannot change status of deleted service"); + require(_isValidStatusTransition(oldStatus, newStatus), "Invalid status transition"); + + service.status = newStatus; + + emit ServiceStatusChanged(serviceId, oldStatus, newStatus); } - function isServiceRegistered(string memory name) external view returns (bool) { - require(bytes(name).length > 0, "Invalid service name"); + /** + * @dev Soft deletes a service by setting its status to DELETED. + * @param serviceId The ID of the service to delete. + */ + 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); + } + + // ============================================================================ + // 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; + + 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, service.status, 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/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/schemas/service.schemas.ts b/packages/sdk/src/schemas/service.schemas.ts index 97f7a12..65554a6 100644 --- a/packages/sdk/src/schemas/service.schemas.ts +++ b/packages/sdk/src/schemas/service.schemas.ts @@ -69,7 +69,6 @@ export const ServiceOperationalStatusSchema = z.enum(['healthy', 'degraded', 'un */ export const ServiceOnChainSchema = z.object({ id: z.string().describe('Auto-incremented service ID from blockchain'), - name: z.string().min(1, 'Service name is required').max(100, 'Service name too long'), owner: EthereumAddressSchema, agentAddress: z.string().regex(ethereumAddressRegex, 'Invalid agent address format').optional(), serviceUri: z.string().describe('IPFS URI for service metadata'), @@ -82,6 +81,7 @@ export const ServiceOnChainSchema = z.object({ */ 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, @@ -162,7 +162,7 @@ export const RegisterServiceParamsSchema = z.object({ agentAddress: z.string().regex(ethereumAddressRegex, 'Invalid agent address format').optional(), // Off-chain metadata (will be stored in IPFS) - metadata: ServiceMetadataSchema.omit({ operational: true, createdAt: true, updatedAt: true }) + metadata: ServiceMetadataSchema.omit({ name: true, operational: true, createdAt: true, updatedAt: true }) }).partial({ agentAddress: true // Optional until service is published }); @@ -180,7 +180,7 @@ export const UpdateServiceParamsSchema = z.object({ status: ServiceStatusSchema.optional(), // Off-chain metadata updates (optional) - metadata: ServiceMetadataSchema.omit({ operational: true, createdAt: true, updatedAt: true }).partial().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 diff --git a/packages/sdk/src/services/ServiceRegistryService.ts b/packages/sdk/src/services/ServiceRegistryService.ts index b334d9b..d5a0b64 100644 --- a/packages/sdk/src/services/ServiceRegistryService.ts +++ b/packages/sdk/src/services/ServiceRegistryService.ts @@ -83,9 +83,10 @@ export class ServiceRegistryService { console.log(`Registering service: ${parsedParams.name}`); - // Add timestamps to metadata before upload + // Add name and timestamps to metadata before upload const metadataWithTimestamps = { ...parsedParams.metadata, + name: parsedParams.name, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }; @@ -102,7 +103,6 @@ export class ServiceRegistryService { // Register service on blockchain with minimal data const tx = await this.serviceRegistry.registerService( - parsedParams.name, serviceUri, parsedParams.agentAddress || ethers.ZeroAddress ); @@ -118,14 +118,13 @@ export class ServiceRegistryService { const serviceRecord: ServiceRecord = { // On-chain fields id: serviceId, - name: parsedParams.name, owner: ownerAddress, agentAddress: parsedParams.agentAddress || ethers.ZeroAddress, serviceUri, status: 'draft' as ServiceStatus, version: 1, - // Off-chain fields from metadata (including timestamps) + // Off-chain fields from metadata (including name and timestamps) ...metadataWithTimestamps }; @@ -198,12 +197,46 @@ export class ServiceRegistryService { console.log(`Updating service: ${serviceId}`); - // Update service on blockchain - // Note: This will need to be updated when smart contract supports V2 operations + // 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( - updatedService.name, - updatedService.category, - updatedService.description + serviceId, + serviceUri ); const receipt = await tx.wait(); @@ -494,14 +527,20 @@ export class ServiceRegistryService { } /** - * Unassigns the current agent from a service + * 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): Promise { + 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); @@ -517,19 +556,20 @@ export class ServiceRegistryService { ); } - console.log(`Unassigning agent from service: ${service.agentAddress} <- ${serviceId}`); + console.log(`Unassigning agent from service: ${service.agentAddress} <- ${serviceId} (status: ${newStatus})`); - // Update service to remove agent assignment - const updatedService: ServiceRecord = { - ...service, - agentAddress: ethers.ZeroAddress, - updatedAt: new Date().toISOString() - }; - - // This will need smart contract support for agent unassignment - console.log(`Agent unassigned from service: ${serviceId}`); + // 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 + ); - return updatedService; + 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; @@ -664,35 +704,12 @@ export class ServiceRegistryService { /** * Legacy method: Get service by name (V1 compatibility) - * @deprecated Use getServiceById() instead + * @deprecated Use getServiceById() instead. V2 services don't support name-based lookup. */ async getService(name: string): Promise { - console.warn("getService() by name is deprecated. Use getServiceById() instead."); - - // This is a placeholder - in reality we'd need a name->ID mapping - const contractResult = await this.serviceRegistry.getService(name); - - // Convert contract result to ServiceRecord type (incomplete - needs proper mapping) - const service: ServiceRecord = { - id: crypto.randomUUID(), // Placeholder - should come from contract - name: contractResult.name, - owner: ethers.ZeroAddress, // Placeholder - should come from contract - agentAddress: ethers.ZeroAddress, // Placeholder - should come from contract - serviceUri: "data://placeholder", // Placeholder - should come from contract - status: "draft" as any, // Placeholder - should come from contract - version: 1, // Placeholder - should come from contract - createdAt: new Date().toISOString(), // Placeholder - updatedAt: new Date().toISOString(), // Placeholder - // Off-chain metadata fields - description: contractResult.description, - category: contractResult.category as any, - endpointSchema: "", // Placeholder - should come from metadata - method: "HTTP_POST" as any, // Placeholder - should come from metadata - parametersSchema: {}, // Placeholder - should come from metadata - resultSchema: {}, // Placeholder - should come from metadata - pricing: undefined, // Placeholder - should come from metadata - }; - - return service; + throw new Error( + "getService() by name is no longer supported in V2. " + + "Service names are stored off-chain. Use getServiceById() instead." + ); } } From d8e4412de9f9f99abb59e0df3a3bb2c3ad681d16 Mon Sep 17 00:00:00 2001 From: Leon Prouger Date: Thu, 28 Aug 2025 12:49:57 +0300 Subject: [PATCH 14/22] removing the proposals --- .../contracts/AgentsRegistryUpgradeable.sol | 185 +---- .../contracts/TaskRegistryUpgradeable.sol | 183 +++-- .../contracts/interfaces/IProposalStruct.sol | 12 - packages/contracts/test/AgentRegistry.test.js | 750 ------------------ .../contracts/test/ServiceRegistry.test.js | 70 -- packages/contracts/test/TaskRegistry.test.js | 473 ----------- 6 files changed, 120 insertions(+), 1553 deletions(-) delete mode 100644 packages/contracts/contracts/interfaces/IProposalStruct.sol delete mode 100644 packages/contracts/test/AgentRegistry.test.js delete mode 100644 packages/contracts/test/ServiceRegistry.test.js delete mode 100644 packages/contracts/test/TaskRegistry.test.js diff --git a/packages/contracts/contracts/AgentsRegistryUpgradeable.sol b/packages/contracts/contracts/AgentsRegistryUpgradeable.sol index 0cb03af..887393e 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; @@ -41,8 +40,6 @@ contract AgentsRegistryUpgradeable is Initializable, OwnableUpgradeable, UUPSUpg address public taskRegistry; mapping(address => AgentData) public agents; - mapping(uint256 => ServiceProposal) public proposals; - uint256 public nextProposalId; modifier onlyAgentOwner(address agent) { require( @@ -71,7 +68,6 @@ contract AgentsRegistryUpgradeable is Initializable, OwnableUpgradeable, UUPSUpg agentRegistryV1 = _agentRegistryV1; serviceRegistry = _serviceRegistry; - nextProposalId = 1; } event AgentRegistered( @@ -81,28 +77,11 @@ 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 @@ -149,70 +128,8 @@ contract AgentsRegistryUpgradeable is Initializable, OwnableUpgradeable, UUPSUpg _createAgent(agent, name, agentUri, msg.sender, 0); } - // V2: registerAgentWithService removed - agents and services are managed independently - // Use registerAgent() followed by service assignment through ServiceRegistry - - /** - * @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) { - // V2: Service validation removed - services use IDs and are managed independently - // 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 { @@ -226,10 +143,14 @@ 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 (called by TaskRegistry). + * @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 @@ -250,6 +171,11 @@ 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; } @@ -266,12 +192,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. @@ -299,7 +219,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: @@ -307,22 +227,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, @@ -340,69 +260,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++; - } - - // V2: Migration function disabled - services now use different structure - function _ensureServiceRegistered(string memory serviceName) private { - // V2: Disabled - ServiceRegistry V2 uses IDs and different parameters - // The new registerService takes (name, serviceUri, agentAddress) - // This migration logic would need complete rewrite for V2 - } - - 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; - } - - // V2: Service migration disabled - // _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 @@ -413,4 +270,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/TaskRegistryUpgradeable.sol b/packages/contracts/contracts/TaskRegistryUpgradeable.sol index 59334d0..122cac8 100644 --- a/packages/contracts/contracts/TaskRegistryUpgradeable.sol +++ b/packages/contracts/contracts/TaskRegistryUpgradeable.sol @@ -4,17 +4,16 @@ 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 + * @notice A smart contract that manages task creation and completion between users and agents. + * @dev Upgradeable version using UUPS proxy pattern for Task Management V2 */ -contract TaskRegistryUpgradeable is Initializable, OwnableUpgradeable, UUPSUpgradeable, IProposalStruct { +contract TaskRegistryUpgradeable is Initializable, OwnableUpgradeable, UUPSUpgradeable { enum TaskStatus { CREATED, ASSIGNED, COMPLETED, CANCELED } @@ -24,7 +23,8 @@ contract TaskRegistryUpgradeable is Initializable, OwnableUpgradeable, UUPSUpgra address issuer; TaskStatus status; address assignee; - uint256 proposalId; + uint256 price; + address tokenAddress; string result; uint8 rating; } @@ -39,6 +39,11 @@ contract TaskRegistryUpgradeable is Initializable, OwnableUpgradeable, UUPSUpgra _; } + modifier onlyTaskAssignee(uint256 taskId) { + require(msg.sender == tasks[taskId].assignee, "Not the assignee of the task"); + _; + } + /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); @@ -49,138 +54,148 @@ contract TaskRegistryUpgradeable is Initializable, OwnableUpgradeable, UUPSUpgra * @param _initialTaskId The starting task ID * @param _agentRegistry The address of the agent registry */ - function initialize( - uint256 _initialTaskId, - AgentsRegistryUpgradeable _agentRegistry - ) public initializer { + 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 TaskCreated( + address indexed issuer, + address indexed assignee, + uint256 taskId, + string prompt, + uint256 price, + address tokenAddress + ); event TaskCompleted(uint256 indexed taskId, string result); event TaskRated(uint256 indexed taskId, uint8 rating); + event TaskCanceled(uint256 indexed taskId); /** - * @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. - */ + * @dev Creates a new task and assigns it to an agent. + * @param assignee The address of the agent to assign the task to. + * @param prompt The task description/prompt. + * @param price The payment amount for the task. + * @param tokenAddress The token address for payment (address(0) for ETH). + */ function createTask( + address assignee, 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"); + uint256 price, + address tokenAddress + ) external payable { + require(assignee != address(0), "Invalid assignee address"); + require(bytes(prompt).length > 0, "Prompt cannot be empty"); + + // Handle payment + if (tokenAddress == address(0)) { + require(msg.value == price, "Invalid ETH amount"); } else { - require(msg.value == 0, "No ETH should be sent for ERC20 payments"); - TransferHelper.safeTransferFrom(proposal.tokenAddress, msg.sender, address(this), proposal.price); + require(msg.value == 0, "ETH not accepted for token payments"); + TransferHelper.safeTransferFrom(tokenAddress, msg.sender, address(this), price); } + nextTaskId++; 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); + task.assignee = assignee; + task.price = price; + task.tokenAddress = tokenAddress; - nextTaskId++; - return task; + issuerTasks[msg.sender].push(nextTaskId); + + emit TaskCreated(msg.sender, assignee, nextTaskId, prompt, price, tokenAddress); } /** - * @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 { + * @dev Completes a task and releases payment to the assignee. + * @param taskId The ID of the task to complete. + * @param result The task completion result/output. + */ + function completeTask(uint256 taskId, string memory result) external onlyTaskAssignee(taskId) { TaskData storage task = tasks[taskId]; - require(msg.sender == task.assignee, "Not authorized"); - require(task.status == TaskStatus.ASSIGNED, "Invalid task status"); + require(task.status == TaskStatus.ASSIGNED, "Task is not in assigned state"); - 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); + task.status = TaskStatus.COMPLETED; + + // Release payment + if (task.tokenAddress == address(0)) { + TransferHelper.safeTransferETH(task.assignee, task.price); } else { - TransferHelper.safeTransfer(proposal.tokenAddress, task.issuer, proposal.price); + TransferHelper.safeTransfer(task.tokenAddress, task.assignee, task.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 { + * @dev Rates a completed task. + * @param taskId The ID of the task to rate. + * @param rating The rating value (0-100). + */ + function rateTask(uint256 taskId, uint8 rating) external onlyTaskIssuer(taskId) { 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"); - + require(rating <= 100, "Rating must be between 0 and 100"); + task.rating = rating; - ServiceProposal memory proposal = agentRegistry.getProposal(task.proposalId); - agentRegistry.addRating(proposal.issuer, rating); + // Add rating to agent's reputation + agentRegistry.addRating(task.assignee, 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. - */ + * @dev Cancels a task and refunds payment to the issuer. + * @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"); - + require(task.status == TaskStatus.ASSIGNED, "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); + + // Refund payment + if (task.tokenAddress == address(0)) { + TransferHelper.safeTransferETH(task.issuer, task.price); } else { - TransferHelper.safeTransfer(proposal.tokenAddress, task.issuer, proposal.price); + TransferHelper.safeTransfer(task.tokenAddress, task.issuer, task.price); } - - emit TaskStatusChanged(taskId, task.status); - emit TaskCanceled(taskId); - } - function getTasksByIssuer(address issuer) external view returns (uint256[] memory) { - return issuerTasks[issuer]; + emit TaskCanceled(taskId); } + /** + * @dev Gets task data by ID. + * @param taskId The ID of the task. + * @return TaskData The task data. + */ function getTask(uint256 taskId) external view returns (TaskData memory) { return tasks[taskId]; } - function getStatus(uint256 taskId) external view returns (TaskStatus) { - return tasks[taskId].status; + /** + * @dev Gets tasks created by a specific issuer. + * @param issuer The address of the task issuer. + * @return Array of task IDs. + */ + function getTasksByIssuer(address issuer) external view returns (uint256[] memory) { + return issuerTasks[issuer]; } - - function getAssignee(uint256 taskId) external view returns (address) { - return tasks[taskId].assignee; + + /** + * @dev Gets the next task ID. + * @return The next task ID. + */ + function getNextTaskId() external view returns (uint256) { + return nextTaskId + 1; } /** @@ -193,4 +208,4 @@ contract TaskRegistryUpgradeable is Initializable, OwnableUpgradeable, UUPSUpgra * @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/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/test/AgentRegistry.test.js b/packages/contracts/test/AgentRegistry.test.js deleted file mode 100644 index de2b732..0000000 --- a/packages/contracts/test/AgentRegistry.test.js +++ /dev/null @@ -1,750 +0,0 @@ -const { expect } = require("chai"); -const { ethers, upgrades } = require("hardhat"); -const AgentRegistryV1Artifact = require('./artifacts/AgentsRegistryV1.json') - -describe("AgentsRegistryUpgradeable", function () { - let AgentRegistry; - let agentRegistryV1 - let registry; - let serviceRegistry; - let serviceRegistryV1; - let admin, agentOwner, agentAddress; - let agentUri = "https://ipfs.io/ipfs/bafkreigzpb44ndvlsfazfymmf6yvquoregceik56vyskf7e35joel7yati"; - - beforeEach(async function () { - [admin, 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"); - serviceRegistryV1 = await upgrades.deployProxy(ServiceRegistryV1, [], { - initializer: "initialize", - kind: "uups" - }); - - 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('#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 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); - }); - }) - - describe('#registerAgent', () => { - it("Should register a new agent without proposal", async function () { - const request = registry.connect(agentOwner).registerAgent( - agentAddress, - "Simple Agent", - agentUri - ); - - await expect(request) - .to.emit(registry, "AgentRegistered") - .withArgs(agentAddress, agentOwner, "Simple Agent", agentUri); - - const agentData = await registry.getAgentData(agentAddress); - - 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); - 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 - ); - - await expect( - registry.connect(agentOwner).registerAgent( - agentAddress, - "Another Agent", - agentUri - ) - ).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 - ); - - await registry.connect(agentOwner).registerAgent( - eveAddress, - "Second Agent", - agentUri - ); - - const firstAgentData = await registry.getAgentData(agentAddress); - const secondAgentData = await registry.getAgentData(eveAddress); - - 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"); - }); - - it("Should handle agent address same as owner address", async function () { - await registry.connect(agentOwner).registerAgent( - agentOwner.address, - "Self Agent", - agentUri - ); - - 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)); - - await registry.connect(agentOwner).registerAgent( - agentAddress, - longName, - longUri - ); - - const agentData = await registry.getAgentData(agentAddress); - expect(agentData.name).to.equal(longName); - expect(agentData.agentUri).to.equal(longUri); - }); - - it("Should not create any proposals when using registerAgent", async function () { - await registry.connect(agentOwner).registerAgent( - agentAddress, - "Simple Agent", - agentUri - ); - - // 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 () { - await expect( - registry.connect(agentOwner).registerAgentWithService( - agentAddress, - "Service Agent", - agentUri, - "NonExistentService", - ethers.parseEther("0.01"), - ethers.ZeroAddress - ) - ).to.be.revertedWith("Service not registered"); - }); - - 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 - ); - - await expect( - registry.connect(agentOwner).registerAgentWithService( - agentAddress, - "Service Agent", - agentUri, - "Service1", - ethers.parseEther("0.01"), - ethers.ZeroAddress - ) - ).to.be.revertedWith("Agent already registered"); - }); - }) - - - describe('#Proposals', () => { - - beforeEach(async function () { - await registry.connect(agentOwner).registerAgentWithService( - agentAddress, - "Service Agent", - agentUri, - "Service1", - ethers.parseEther("0.01"), - ethers.ZeroAddress - ); - }); - - - 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 registry.connect(agentOwner).removeProposal(agentAddress, 1); - - expect(await registry.getProposal(1)).to.deep.equal([ - ethers.ZeroAddress, - "", - 0, - ethers.ZeroAddress, - 0, - false - ]); - }); - }); - - - 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 add a rating", async function () { - await registry.connect(admin).setTaskRegistry(admin); - - 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); - - 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); - 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") - ); - - await expect( - registry.connect(eveAddress).migrateAgent(agentAddress) - ).to.be.revertedWith("Not owner or agent owner"); - - await registry.migrateAgent(agentAddress) - - 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); - expect(agentData.totalRatings).to.equal(0); - }); - }) - - describe('#SetAgentData', () => { - const newAgentName = "Updated Agent Name"; - const newAgentUri = "https://ipfs.io/ipfs/updated-hash"; - - beforeEach(async function () { - await registry.connect(agentOwner).registerAgentWithService( - agentAddress, - "Service Agent", - agentUri, - "Service1", - ethers.parseEther("0.01"), - ethers.ZeroAddress - ); - }); - - it("Should successfully update agent data", async function () { - const updateTx = registry.connect(agentOwner).setAgentData( - agentAddress, - newAgentName, - newAgentUri - ); - - await expect(updateTx) - .to.emit(registry, "AgentDataUpdated") - .withArgs(agentAddress, newAgentName, newAgentUri); - - 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); - }); - - it("Should not allow non-owner to update agent data", async function () { - const [, , , unauthorizedUser] = await ethers.getSigners(); - - await expect( - registry.connect(unauthorizedUser).setAgentData( - agentAddress, - newAgentName, - newAgentUri - ) - ).to.be.revertedWith("Not the owner of the agent"); - }); - - it("Should not allow updating unregistered agent", async function () { - const [, , , , unregisteredAgent] = await ethers.getSigners(); - - await expect( - registry.connect(unregisteredAgent).setAgentData( - unregisteredAgent.address, - newAgentName, - newAgentUri - ) - ).to.be.revertedWith("Not the owner of the agent"); - }); - - it("Should allow updating with empty strings", async function () { - const emptyName = ""; - const emptyUri = ""; - - const updateTx = registry.connect(agentOwner).setAgentData( - agentAddress, - emptyName, - emptyUri - ); - - 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); - }); - - 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 - ); - - 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 - ); - - 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 - ); - - // 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" - ); - - agentData = await registry.getAgentData(agentAddress); - expect(agentData.name).to.equal("Second Update"); - expect(agentData.agentUri).to.equal("https://second-update.com"); - }); - }); - - describe('#RemoveAgent', () => { - 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 - ); - }); - - 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(); - - await expect( - registry.connect(unauthorizedUser).removeAgent(agentAddress) - ).to.be.revertedWith("Not the owner of the agent"); - }); - - it("Should not allow removing non-existent agent", async function () { - const [, , , , unregisteredAgent] = await ethers.getSigners(); - - await expect( - registry.connect(unregisteredAgent).removeAgent(unregisteredAgent.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 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 - ); - - // 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); - }); - - 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); - - // 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); - }); - - 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); - - // 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); - - 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); - }); - }); -}); diff --git a/packages/contracts/test/ServiceRegistry.test.js b/packages/contracts/test/ServiceRegistry.test.js deleted file mode 100644 index 74339f3..0000000 --- a/packages/contracts/test/ServiceRegistry.test.js +++ /dev/null @@ -1,70 +0,0 @@ -const { expect } = require("chai"); -const { ethers, upgrades } = require("hardhat"); - -describe("ServiceRegistryUpgradeable", function () { - let ServiceRegistry; - let registry; - let owner, addr1; - - let service1 = "Service1"; - let category1 = "Category1"; - let description1 = "Description1"; - - let category2 = "Category2"; - let description2 = "Description2"; - - beforeEach(async function () { - [owner, addr1] = await ethers.getSigners(); - ServiceRegistry = await ethers.getContractFactory("ServiceRegistryUpgradeable"); - registry = await upgrades.deployProxy(ServiceRegistry, [], { - initializer: "initialize", - kind: "uups" - }); - }); - - it("Should register a new service", async function () { - const request = registry.registerService(service1, category1, description1) - - await expect(request) - .to.emit(registry, "ServiceRegistered") - .withArgs(service1, category1, description1); - - const isRegistered = await registry.isServiceRegistered(service1); - expect(isRegistered).to.be.true; - - 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 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"); - }); - - it("Should not register a service with an empty name", async function () { - await expect(registry.registerService("", category1, description1)).to.be.revertedWith("Invalid service name"); - }); - - 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"); - }); - - - it("Should update a service", async function () { - await registry.registerService(service1, category1, description1); - const request = registry.updateService(service1, category2, description2); - - await expect(request) - .to.emit(registry, "ServiceUpdated") - .withArgs(service1, category2, description2); - - const service = await registry.getService(service1); - expect(service.name).to.equal(service1); - expect(service.category).to.equal(category2); - expect(service.description).to.equal(description2); - - }); - -}) \ 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"); - }); - }); - - }); -}); From d32c304eb4d5d6ca9bbf77716d6aac8b1f943fbd Mon Sep 17 00:00:00 2001 From: Leon Prouger Date: Thu, 28 Aug 2025 12:50:39 +0300 Subject: [PATCH 15/22] updating tests --- .../contracts/test/ServiceRegistry.test.js | 293 ++++++++++++++++++ 1 file changed, 293 insertions(+) create mode 100644 packages/contracts/test/ServiceRegistry.test.js diff --git a/packages/contracts/test/ServiceRegistry.test.js b/packages/contracts/test/ServiceRegistry.test.js new file mode 100644 index 0000000..a4ae748 --- /dev/null +++ b/packages/contracts/test/ServiceRegistry.test.js @@ -0,0 +1,293 @@ +const { expect } = require("chai"); +const { ethers, upgrades } = require("hardhat"); + +describe("ServiceRegistryUpgradeable V2 - Phase 1: Core Functionality", function () { + let ServiceRegistry; + let registry; + let owner, addr1, addr2, agentAddr1, agentAddr2; + + // 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, addr2, agentAddr1, agentAddr2] = await ethers.getSigners(); + ServiceRegistry = await ethers.getContractFactory("ServiceRegistryUpgradeable"); + registry = await upgrades.deployProxy(ServiceRegistry, [], { + initializer: "initialize", + kind: "uups" + }); + await registry.waitForDeployment(); + }); + + 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); + }); + + 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); + }); + + it("Should prevent multiple initialization", async function () { + // Try to initialize again - should revert with custom error + await expect(registry.initialize()).to.be.reverted; + }); + + it("Should set correct owner during initialization", async function () { + expect(await registry.owner()).to.equal(owner.address); + }); + }); + + 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); + + 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); + }); + }); + + 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 validate existing service ID", async function () { + // This should not revert + await registry.getService(1); + }); + + it("Should reject service ID 0", async function () { + await expect(registry.getService(0)) + .to.be.revertedWith("Service does not exist"); + }); + + 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"); + }); + }); + + 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 From 29b370750ec8f145876a280b820ce9699b691408 Mon Sep 17 00:00:00 2001 From: Leon Prouger Date: Thu, 28 Aug 2025 23:19:55 +0300 Subject: [PATCH 16/22] Agent Registry fixes and test updates --- .../contracts/AgentsRegistryUpgradeable.sol | 12 +- .../contracts/TaskRegistryUpgradeable.sol | 211 ------------------ .../contracts/ignition/modules/DeployAll.ts | 29 +-- .../modules/TaskRegistryUpgradeable.ts | 44 ---- .../ignition/modules/UpgradeRegistries.ts | 14 +- .../ignition/params/baseSepolia.json | 5 +- packages/contracts/ignition/params/local.json | 5 +- packages/contracts/scripts/create-task.ts | 47 ---- packages/contracts/scripts/deploy.js | 10 +- packages/contracts/scripts/deploy.ts | 29 +-- 10 files changed, 19 insertions(+), 387 deletions(-) delete mode 100644 packages/contracts/contracts/TaskRegistryUpgradeable.sol delete mode 100644 packages/contracts/ignition/modules/TaskRegistryUpgradeable.ts delete mode 100644 packages/contracts/scripts/create-task.ts diff --git a/packages/contracts/contracts/AgentsRegistryUpgradeable.sol b/packages/contracts/contracts/AgentsRegistryUpgradeable.sol index 887393e..8d99070 100644 --- a/packages/contracts/contracts/AgentsRegistryUpgradeable.sol +++ b/packages/contracts/contracts/AgentsRegistryUpgradeable.sol @@ -37,7 +37,6 @@ contract AgentsRegistryUpgradeable is Initializable, OwnableUpgradeable, UUPSUpg IAgentRegistryV1 public agentRegistryV1; ServiceRegistryUpgradeable public serviceRegistry; - address public taskRegistry; mapping(address => AgentData) public agents; @@ -87,14 +86,6 @@ contract AgentsRegistryUpgradeable is Initializable, OwnableUpgradeable, UUPSUpg 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. @@ -146,7 +137,7 @@ contract AgentsRegistryUpgradeable is Initializable, OwnableUpgradeable, UUPSUpg } /** - * @dev Adds a rating to an agent (called by TaskRegistry). + * @dev Adds a rating to an agent (for external integration). * @param agent The address of the agent. * @param _rating The rating value (0-100). * @return The new reputation score. @@ -155,7 +146,6 @@ contract AgentsRegistryUpgradeable is Initializable, OwnableUpgradeable, UUPSUpg address agent, uint256 _rating ) public returns (uint256) { - require(msg.sender == taskRegistry, "Not the TaskRegistry contract"); require( _rating >= 0 && _rating <= 100, "Rating must be between 0 and 100" diff --git a/packages/contracts/contracts/TaskRegistryUpgradeable.sol b/packages/contracts/contracts/TaskRegistryUpgradeable.sol deleted file mode 100644 index 122cac8..0000000 --- a/packages/contracts/contracts/TaskRegistryUpgradeable.sol +++ /dev/null @@ -1,211 +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 "./AgentsRegistryUpgradeable.sol"; -import "./lib/TransferHelper.sol"; - -/** - * @title TaskRegistryUpgradeable - * @author leonprou - * @notice A smart contract that manages task creation and completion between users and agents. - * @dev Upgradeable version using UUPS proxy pattern for Task Management V2 - */ -contract TaskRegistryUpgradeable is Initializable, OwnableUpgradeable, UUPSUpgradeable { - - enum TaskStatus { CREATED, ASSIGNED, COMPLETED, CANCELED } - - struct TaskData { - uint256 id; - string prompt; - address issuer; - TaskStatus status; - address assignee; - uint256 price; - address tokenAddress; - 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"); - _; - } - - modifier onlyTaskAssignee(uint256 taskId) { - require(msg.sender == tasks[taskId].assignee, "Not the assignee 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, - string prompt, - uint256 price, - address tokenAddress - ); - event TaskCompleted(uint256 indexed taskId, string result); - event TaskRated(uint256 indexed taskId, uint8 rating); - event TaskCanceled(uint256 indexed taskId); - - /** - * @dev Creates a new task and assigns it to an agent. - * @param assignee The address of the agent to assign the task to. - * @param prompt The task description/prompt. - * @param price The payment amount for the task. - * @param tokenAddress The token address for payment (address(0) for ETH). - */ - function createTask( - address assignee, - string memory prompt, - uint256 price, - address tokenAddress - ) external payable { - require(assignee != address(0), "Invalid assignee address"); - require(bytes(prompt).length > 0, "Prompt cannot be empty"); - - // Handle payment - if (tokenAddress == address(0)) { - require(msg.value == price, "Invalid ETH amount"); - } else { - require(msg.value == 0, "ETH not accepted for token payments"); - TransferHelper.safeTransferFrom(tokenAddress, msg.sender, address(this), price); - } - - nextTaskId++; - TaskData storage task = tasks[nextTaskId]; - task.id = nextTaskId; - task.prompt = prompt; - task.issuer = msg.sender; - task.status = TaskStatus.ASSIGNED; - task.assignee = assignee; - task.price = price; - task.tokenAddress = tokenAddress; - - issuerTasks[msg.sender].push(nextTaskId); - - emit TaskCreated(msg.sender, assignee, nextTaskId, prompt, price, tokenAddress); - } - - /** - * @dev Completes a task and releases payment to the assignee. - * @param taskId The ID of the task to complete. - * @param result The task completion result/output. - */ - function completeTask(uint256 taskId, string memory result) external onlyTaskAssignee(taskId) { - TaskData storage task = tasks[taskId]; - require(task.status == TaskStatus.ASSIGNED, "Task is not in assigned state"); - - task.result = result; - task.status = TaskStatus.COMPLETED; - - // Release payment - if (task.tokenAddress == address(0)) { - TransferHelper.safeTransferETH(task.assignee, task.price); - } else { - TransferHelper.safeTransfer(task.tokenAddress, task.assignee, task.price); - } - - emit TaskCompleted(taskId, result); - } - - /** - * @dev Rates a completed task. - * @param taskId The ID of the task to rate. - * @param rating The rating value (0-100). - */ - function rateTask(uint256 taskId, uint8 rating) external onlyTaskIssuer(taskId) { - TaskData storage task = tasks[taskId]; - require(task.status == TaskStatus.COMPLETED, "Task is not completed"); - require(rating <= 100, "Rating must be between 0 and 100"); - - task.rating = rating; - - // Add rating to agent's reputation - agentRegistry.addRating(task.assignee, rating); - - emit TaskRated(taskId, rating); - } - - /** - * @dev Cancels a task and refunds payment to the issuer. - * @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.ASSIGNED, "Task cannot be canceled"); - - task.status = TaskStatus.CANCELED; - - // Refund payment - if (task.tokenAddress == address(0)) { - TransferHelper.safeTransferETH(task.issuer, task.price); - } else { - TransferHelper.safeTransfer(task.tokenAddress, task.issuer, task.price); - } - - emit TaskCanceled(taskId); - } - - /** - * @dev Gets task data by ID. - * @param taskId The ID of the task. - * @return TaskData The task data. - */ - function getTask(uint256 taskId) external view returns (TaskData memory) { - return tasks[taskId]; - } - - /** - * @dev Gets tasks created by a specific issuer. - * @param issuer The address of the task issuer. - * @return Array of task IDs. - */ - function getTasksByIssuer(address issuer) external view returns (uint256[] memory) { - return issuerTasks[issuer]; - } - - /** - * @dev Gets the next task ID. - * @return The next task ID. - */ - function getNextTaskId() external view returns (uint256) { - return nextTaskId + 1; - } - - /** - * @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/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 From ffc6e81eb362dbb96e65a2034341f10dfbc5ef4e Mon Sep 17 00:00:00 2001 From: Leon Prouger Date: Thu, 28 Aug 2025 23:20:10 +0300 Subject: [PATCH 17/22] Agent Registry fixes and test updates --- packages/contracts/test/AgentRegistry.test.js | 379 ++++++++++++++++++ 1 file changed, 379 insertions(+) create mode 100644 packages/contracts/test/AgentRegistry.test.js diff --git a/packages/contracts/test/AgentRegistry.test.js b/packages/contracts/test/AgentRegistry.test.js new file mode 100644 index 0000000..af5edf9 --- /dev/null +++ b/packages/contracts/test/AgentRegistry.test.js @@ -0,0 +1,379 @@ +const { expect } = require("chai"); +const { ethers, upgrades } = require("hardhat"); + +describe("AgentsRegistryUpgradeable V2 - Without Proposals", function () { + let AgentsRegistry; + let ServiceRegistry; + let agentsRegistry; + let serviceRegistry; + 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 () { + [owner, addr1, addr2, agentAddr1, agentAddr2] = await ethers.getSigners(); + + // Deploy ServiceRegistry first (dependency) + ServiceRegistry = await ethers.getContractFactory("ServiceRegistryUpgradeable"); + serviceRegistry = await upgrades.deployProxy(ServiceRegistry, [], { + initializer: "initialize", + kind: "uups" + }); + await serviceRegistry.waitForDeployment(); + + // 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(); + }); + + 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()); + }); + + 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 prevent multiple initialization", async function () { + await expect( + agentsRegistry.initialize(await mockV1Registry.getAddress(), await serviceRegistry.getAddress()) + ).to.be.reverted; + }); + }); + + 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(tx) + .to.emit(agentsRegistry, "AgentRegistered") + .withArgs(agentAddr1.address, addr1.address, AGENT_NAME_1, AGENT_URI_1); + + 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 fail to register an agent twice", async function () { + await agentsRegistry.connect(addr1).registerAgent( + agentAddr1.address, + AGENT_NAME_1, + AGENT_URI_1 + ); + + await expect( + agentsRegistry.connect(addr2).registerAgent( + agentAddr1.address, + AGENT_NAME_2, + AGENT_URI_2 + ) + ).to.be.revertedWith("Agent already registered"); + }); + + 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 agentsRegistry.connect(addr2).registerAgent( + agentAddr2.address, + AGENT_NAME_2, + AGENT_URI_2 + ); + + const agent1Data = await agentsRegistry.getAgentData(agentAddr1.address); + const agent2Data = await agentsRegistry.getAgentData(agentAddr2.address); + + expect(agent1Data.owner).to.equal(addr1.address); + expect(agent2Data.owner).to.equal(addr2.address); + }); + }); + + describe("3. Agent Data Management", function () { + beforeEach(async function () { + await agentsRegistry.connect(addr1).registerAgent( + agentAddr1.address, + AGENT_NAME_1, + AGENT_URI_1 + ); + }); + + it("Should update agent data by owner", async function () { + const newName = "Updated Agent Name"; + const newUri = "ipfs://QmUpdatedHash/metadata.json"; + + const tx = await agentsRegistry.connect(addr1).setAgentData( + agentAddr1.address, + newName, + newUri + ); + + await expect(tx) + .to.emit(agentsRegistry, "AgentDataUpdated") + .withArgs(agentAddr1.address, newName, newUri); + + const agentData = await agentsRegistry.getAgentData(agentAddr1.address); + expect(agentData.name).to.equal(newName); + expect(agentData.agentUri).to.equal(newUri); + }); + + it("Should fail to update agent data by non-owner", async function () { + await expect( + agentsRegistry.connect(addr2).setAgentData( + agentAddr1.address, + "New Name", + "New URI" + ) + ).to.be.revertedWith("Not the owner of the agent"); + }); + + it("Should fail to update non-existent agent", async function () { + await expect( + agentsRegistry.connect(addr1).setAgentData( + agentAddr2.address, + "New Name", + "New URI" + ) + ).to.be.revertedWith("Not the owner of the agent"); + }); + }); + + describe("4. Agent Reputation System", function () { + beforeEach(async function () { + 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.addRating(agentAddr1.address, rating); + + await expect(tx) + .to.emit(agentsRegistry, "ReputationUpdated") + .withArgs(agentAddr1.address, rating); + + const reputation = await agentsRegistry.getReputation(agentAddr1.address); + expect(reputation).to.equal(rating); + + const agentData = await agentsRegistry.getAgentData(agentAddr1.address); + expect(agentData.totalRatings).to.equal(1); + }); + + it("Should calculate average reputation correctly", async function () { + await agentsRegistry.addRating(agentAddr1.address, 80); + await agentsRegistry.addRating(agentAddr1.address, 90); + await agentsRegistry.addRating(agentAddr1.address, 100); + + const reputation = await agentsRegistry.getReputation(agentAddr1.address); + expect(reputation).to.equal(90); // (80 + 90 + 100) / 3 = 90 + + const agentData = await agentsRegistry.getAgentData(agentAddr1.address); + expect(agentData.totalRatings).to.equal(3); + }); + + it("Should reject invalid ratings", async function () { + await expect( + agentsRegistry.addRating(agentAddr1.address, 101) + ).to.be.revertedWith("Rating must be between 0 and 100"); + }); + + it("Should handle edge case ratings", async function () { + await agentsRegistry.addRating(agentAddr1.address, 0); + let reputation = await agentsRegistry.getReputation(agentAddr1.address); + expect(reputation).to.equal(0); + + await agentsRegistry.addRating(agentAddr1.address, 100); + reputation = await agentsRegistry.getReputation(agentAddr1.address); + expect(reputation).to.equal(50); // (0 + 100) / 2 = 50 + }); + }); + + describe("5. Agent Removal", function () { + beforeEach(async function () { + await agentsRegistry.connect(addr1).registerAgent( + agentAddr1.address, + AGENT_NAME_1, + AGENT_URI_1 + ); + }); + + it("Should remove agent by owner", async function () { + const tx = await agentsRegistry.connect(addr1).removeAgent(agentAddr1.address); + + await expect(tx) + .to.emit(agentsRegistry, "AgentRemoved") + .withArgs(agentAddr1.address, addr1.address); + + const agentData = await agentsRegistry.getAgentData(agentAddr1.address); + expect(agentData.agent).to.equal(ZERO_ADDRESS); + expect(agentData.name).to.equal(""); + }); + + it("Should fail to remove agent by non-owner", async function () { + await expect( + agentsRegistry.connect(addr2).removeAgent(agentAddr1.address) + ).to.be.revertedWith("Not the owner of the agent"); + }); + + it("Should fail to remove non-existent agent", async function () { + await expect( + agentsRegistry.connect(addr1).removeAgent(agentAddr2.address) + ).to.be.revertedWith("Not the owner of the agent"); + }); + + it("Should allow re-registration after removal", async function () { + await agentsRegistry.connect(addr1).removeAgent(agentAddr1.address); + + // Should be able to register the same agent address again + await agentsRegistry.connect(addr2).registerAgent( + agentAddr1.address, + AGENT_NAME_2, + AGENT_URI_2 + ); + + const agentData = await agentsRegistry.getAgentData(agentAddr1.address); + expect(agentData.owner).to.equal(addr2.address); + expect(agentData.name).to.equal(AGENT_NAME_2); + }); + }); + + 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(); + + await agentsRegistry.connect(owner).setServiceRegistry( + await newServiceRegistry.getAddress() + ); + + expect(await agentsRegistry.serviceRegistry()).to.equal( + await newServiceRegistry.getAddress() + ); + }); + + it("Should fail to set service registry by non-owner", async function () { + await expect( + agentsRegistry.connect(addr1).setServiceRegistry(addr2.address) + ).to.be.reverted; + }); + + 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("7. Access Control", function () { + beforeEach(async function () { + await agentsRegistry.connect(addr1).registerAgent( + agentAddr1.address, + AGENT_NAME_1, + AGENT_URI_1 + ); + }); + + it("Should enforce onlyAgentOwner modifier", async function () { + // Try to update agent data as non-owner + await expect( + agentsRegistry.connect(addr2).setAgentData( + agentAddr1.address, + "New Name", + "New URI" + ) + ).to.be.revertedWith("Not the owner of the agent"); + + // Try to remove agent as non-owner + await expect( + agentsRegistry.connect(addr2).removeAgent(agentAddr1.address) + ).to.be.revertedWith("Not the owner of the agent"); + }); + + it("Should enforce onlyOwner modifier", async function () { + await expect( + agentsRegistry.connect(addr1).setServiceRegistry(addr2.address) + ).to.be.reverted; + }); + }); + + describe("8. Data Retrieval", function () { + beforeEach(async function () { + await agentsRegistry.connect(addr1).registerAgent( + agentAddr1.address, + AGENT_NAME_1, + AGENT_URI_1 + ); + await agentsRegistry.addRating(agentAddr1.address, 75); + }); + + it("Should retrieve complete agent data", async function () { + 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(75); + expect(agentData.totalRatings).to.equal(1); + }); + + it("Should retrieve reputation separately", async function () { + const reputation = await agentsRegistry.getReputation(agentAddr1.address); + expect(reputation).to.equal(75); + }); + + it("Should return zero values for non-existent agent", async function () { + const agentData = await agentsRegistry.getAgentData(agentAddr2.address); + + 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 From 746072db98273745f4d686e72c9c0771d2ef9a76 Mon Sep 17 00:00:00 2001 From: Leon Prouger Date: Thu, 28 Aug 2025 23:26:44 +0300 Subject: [PATCH 18/22] make sure user can rate agent once --- .../contracts/AgentsRegistryUpgradeable.sol | 27 +++++++- packages/contracts/test/AgentRegistry.test.js | 63 ++++++++++++++++--- 2 files changed, 80 insertions(+), 10 deletions(-) diff --git a/packages/contracts/contracts/AgentsRegistryUpgradeable.sol b/packages/contracts/contracts/AgentsRegistryUpgradeable.sol index 8d99070..e69d2d4 100644 --- a/packages/contracts/contracts/AgentsRegistryUpgradeable.sol +++ b/packages/contracts/contracts/AgentsRegistryUpgradeable.sol @@ -39,6 +39,8 @@ contract AgentsRegistryUpgradeable is Initializable, OwnableUpgradeable, UUPSUpg ServiceRegistryUpgradeable public serviceRegistry; mapping(address => AgentData) public agents; + // 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( @@ -137,7 +139,7 @@ contract AgentsRegistryUpgradeable is Initializable, OwnableUpgradeable, UUPSUpg } /** - * @dev Adds a rating to an agent (for external integration). + * @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. @@ -146,10 +148,23 @@ contract AgentsRegistryUpgradeable is Initializable, OwnableUpgradeable, UUPSUpg address agent, uint256 _rating ) public returns (uint256) { + 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 * @@ -170,6 +185,16 @@ contract AgentsRegistryUpgradeable is Initializable, OwnableUpgradeable, UUPSUpg 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. diff --git a/packages/contracts/test/AgentRegistry.test.js b/packages/contracts/test/AgentRegistry.test.js index af5edf9..18a9a11 100644 --- a/packages/contracts/test/AgentRegistry.test.js +++ b/packages/contracts/test/AgentRegistry.test.js @@ -186,7 +186,7 @@ describe("AgentsRegistryUpgradeable V2 - Without Proposals", function () { it("Should add rating to agent", async function () { const rating = 85; - const tx = await agentsRegistry.addRating(agentAddr1.address, rating); + const tx = await agentsRegistry.connect(addr2).addRating(agentAddr1.address, rating); await expect(tx) .to.emit(agentsRegistry, "ReputationUpdated") @@ -197,12 +197,31 @@ describe("AgentsRegistryUpgradeable V2 - Without Proposals", function () { 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; }); - it("Should calculate average reputation correctly", async function () { - await agentsRegistry.addRating(agentAddr1.address, 80); - await agentsRegistry.addRating(agentAddr1.address, 90); - await agentsRegistry.addRating(agentAddr1.address, 100); + 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 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); const reputation = await agentsRegistry.getReputation(agentAddr1.address); expect(reputation).to.equal(90); // (80 + 90 + 100) / 3 = 90 @@ -211,21 +230,47 @@ describe("AgentsRegistryUpgradeable V2 - Without Proposals", function () { expect(agentData.totalRatings).to.equal(3); }); + it("Should reject rating for non-existent agent", async function () { + await expect( + agentsRegistry.connect(addr1).addRating(agentAddr2.address, 80) + ).to.be.revertedWith("Agent not registered"); + }); + it("Should reject invalid ratings", async function () { await expect( - agentsRegistry.addRating(agentAddr1.address, 101) + agentsRegistry.connect(addr2).addRating(agentAddr1.address, 101) ).to.be.revertedWith("Rating must be between 0 and 100"); }); it("Should handle edge case ratings", async function () { - await agentsRegistry.addRating(agentAddr1.address, 0); + await agentsRegistry.connect(addr1).addRating(agentAddr1.address, 0); let reputation = await agentsRegistry.getReputation(agentAddr1.address); expect(reputation).to.equal(0); - await agentsRegistry.addRating(agentAddr1.address, 100); + await agentsRegistry.connect(addr2).addRating(agentAddr1.address, 100); reputation = await agentsRegistry.getReputation(agentAddr1.address); expect(reputation).to.equal(50); // (0 + 100) / 2 = 50 }); + + 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 () { @@ -346,7 +391,7 @@ describe("AgentsRegistryUpgradeable V2 - Without Proposals", function () { AGENT_NAME_1, AGENT_URI_1 ); - await agentsRegistry.addRating(agentAddr1.address, 75); + await agentsRegistry.connect(addr2).addRating(agentAddr1.address, 75); }); it("Should retrieve complete agent data", async function () { From 12d51f805f17cbdabd3af10903c3708f784cb949 Mon Sep 17 00:00:00 2001 From: Leon Prouger Date: Thu, 28 Aug 2025 23:44:14 +0300 Subject: [PATCH 19/22] Adding contracts tests --- .../contracts/ServiceRegistryUpgradeable.sol | 32 +++++++++++++------ 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/packages/contracts/contracts/ServiceRegistryUpgradeable.sol b/packages/contracts/contracts/ServiceRegistryUpgradeable.sol index c22e2f7..5057806 100644 --- a/packages/contracts/contracts/ServiceRegistryUpgradeable.sol +++ b/packages/contracts/contracts/ServiceRegistryUpgradeable.sol @@ -136,6 +136,8 @@ contract ServiceRegistryUpgradeable is Initializable, OwnableUpgradeable, UUPSUp 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"); @@ -144,28 +146,39 @@ contract ServiceRegistryUpgradeable is Initializable, OwnableUpgradeable, UUPSUp emit ServiceUpdated(serviceId, serviceUri, service.version); } - + /** - * @dev Updates the status of a service. + * @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 updateServiceStatus(uint256 serviceId, ServiceStatus newStatus) - external - serviceExists(serviceId) - onlyServiceOwner(serviceId) + function setServiceStatus(uint256 serviceId, ServiceStatus newStatus) + external + serviceExists(serviceId) + onlyServiceOwner(serviceId) { Service storage service = services[serviceId]; ServiceStatus oldStatus = service.status; - require(oldStatus != ServiceStatus.DELETED, "Cannot change status of deleted service"); - require(_isValidStatusTransition(oldStatus, newStatus), "Invalid status transition"); + // 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 Soft deletes a service by setting its status to DELETED. * @param serviceId The ID of the service to delete. @@ -266,6 +279,7 @@ contract ServiceRegistryUpgradeable is Initializable, OwnableUpgradeable, UUPSUp Service storage service = services[serviceId]; address oldAgent = service.agentAddress; + ServiceStatus oldStatus = service.status; require(oldAgent != address(0), "No agent assigned"); @@ -276,7 +290,7 @@ contract ServiceRegistryUpgradeable is Initializable, OwnableUpgradeable, UUPSUp _removeFromAgentServices(oldAgent, serviceId); emit ServiceAgentUnassigned(serviceId, oldAgent); - emit ServiceStatusChanged(serviceId, service.status, newStatus); + emit ServiceStatusChanged(serviceId, oldStatus, newStatus); } // ============================================================================ From c082cb5d9a3f0cd37e9fcfda6aea7ff8a4cf3c7a Mon Sep 17 00:00:00 2001 From: Leon Prouger Date: Thu, 28 Aug 2025 23:44:58 +0300 Subject: [PATCH 20/22] phase2 tests --- .../test/ServiceRegistryV2.Phase2.test.js | 430 ++++++++++++++++++ 1 file changed, 430 insertions(+) create mode 100644 packages/contracts/test/ServiceRegistryV2.Phase2.test.js 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 From aadfbd9a85d298d95bf3c9f7d7a2b6b6530acb0e Mon Sep 17 00:00:00 2001 From: Leon Prouger Date: Fri, 29 Aug 2025 00:02:06 +0300 Subject: [PATCH 21/22] Updating Service SDK and tests --- .taskmaster/tasks/tasks.json | 6 +- .../src/services/ServiceRegistryService.ts | 297 ++++++++++++++++-- 2 files changed, 282 insertions(+), 21 deletions(-) diff --git a/.taskmaster/tasks/tasks.json b/.taskmaster/tasks/tasks.json index 79867e4..f2ac5b3 100644 --- a/.taskmaster/tasks/tasks.json +++ b/.taskmaster/tasks/tasks.json @@ -374,7 +374,7 @@ "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": "pending", + "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." }, { @@ -385,7 +385,7 @@ "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": "pending", + "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." }, { @@ -418,7 +418,7 @@ ], "metadata": { "created": "2025-07-20T10:42:18.955Z", - "updated": "2025-08-27T17:49:46.813Z", + "updated": "2025-08-28T20:57:44.965Z", "description": "Tasks for master context" } } diff --git a/packages/sdk/src/services/ServiceRegistryService.ts b/packages/sdk/src/services/ServiceRegistryService.ts index d5a0b64..62f5553 100644 --- a/packages/sdk/src/services/ServiceRegistryService.ts +++ b/packages/sdk/src/services/ServiceRegistryService.ts @@ -25,9 +25,26 @@ import { } from "../schemas/service.schemas"; import { ServiceRegistry } from "../../typechain"; -// TODO: Add proper IPFS SDK type when available +// 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, @@ -442,21 +459,21 @@ export class ServiceRegistryService { } /** - * Activates a service (changes status to 'published') + * Publishes a service (changes status to 'published') * @param {string} serviceId - The service ID * @returns {Promise} The updated service */ - async activateService(serviceId: string): Promise { + async publishService(serviceId: string): Promise { return this.updateServiceStatus(serviceId, 'published' as ServiceStatus); } /** - * Deactivates a service (changes status to 'inactive') + * Unpublishes a service (changes status to 'archived') * @param {string} serviceId - The service ID * @returns {Promise} The updated service */ - async deactivateService(serviceId: string): Promise { - return this.updateServiceStatus(serviceId, 'inactive' as ServiceStatus); + async unpublishService(serviceId: string): Promise { + return this.updateServiceStatus(serviceId, 'archived' as ServiceStatus); } /** @@ -509,17 +526,15 @@ export class ServiceRegistryService { console.log(`Assigning agent to service: ${agentAddress} -> ${serviceId}`); - // Update service with agent assignment - const updatedService: ServiceRecord = { - ...service, - agentAddress, - updatedAt: new Date().toISOString() - }; - - // This will need smart contract support for agent assignments + // 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 updatedService; + // Return updated service from contract + return await this.getServiceById(serviceId); } catch (error: any) { console.error(`Error assigning agent to service ${serviceId}:`, error); throw error; @@ -577,7 +592,7 @@ export class ServiceRegistryService { } /** - * Gets all services assigned to a specific agent + * 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 */ @@ -586,7 +601,253 @@ export class ServiceRegistryService { throw new ServiceValidationError(`Invalid agent address: ${agentAddress}`); } - return this.listServices({ 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; + } } // ============================================================================ @@ -706,7 +967,7 @@ export class ServiceRegistryService { * 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 { + 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." From 76921eb0d2107dff305e023d2351d48eb561670e Mon Sep 17 00:00:00 2001 From: Leon Prouger Date: Fri, 29 Aug 2025 00:04:02 +0300 Subject: [PATCH 22/22] deleting legacy methods --- .../src/services/ServiceRegistryService.ts | 30 ------------------- 1 file changed, 30 deletions(-) diff --git a/packages/sdk/src/services/ServiceRegistryService.ts b/packages/sdk/src/services/ServiceRegistryService.ts index 62f5553..fa8eb93 100644 --- a/packages/sdk/src/services/ServiceRegistryService.ts +++ b/packages/sdk/src/services/ServiceRegistryService.ts @@ -932,36 +932,6 @@ export class ServiceRegistryService { // Legacy Methods (for backward compatibility) // ============================================================================ - /** - * Legacy method: Register a service (V1 compatibility) - * @deprecated Use registerService() with RegisterServiceParams instead - */ - async registerServiceLegacy(service: ServiceRecord): Promise { - console.warn("registerServiceLegacy() is deprecated. Use registerService() with RegisterServiceParams instead."); - - try { - // Convert ServiceRecord to RegisterServiceParams format for compatibility - const createParams: RegisterServiceParams = { - name: service.name, - agentAddress: service.agentAddress, - metadata: { - description: service.description, - category: service.category, - endpointSchema: service.endpointSchema, - method: service.method, - parametersSchema: service.parametersSchema, - resultSchema: service.resultSchema, - pricing: service.pricing, - tags: service.tags - } - }; - - await this.registerService(createParams); - return true; - } catch (error) { - return false; - } - } /** * Legacy method: Get service by name (V1 compatibility)