-
Notifications
You must be signed in to change notification settings - Fork 5
Feat/services v2 #67
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Feat/services v2 #67
Conversation
WalkthroughReplaces the Service Registry with Service Registry V2: on‑chain ID‑based ServiceOnChain plus IPFS‑backed off‑chain metadata, full CRUD/lifecycle/agent APIs, Zod schemas/validation, optional IPFS integration, signer-aware write operations, expanded SDK exports/types/errors, updated contracts/ABIs, and removed TaskRegistry and its deployment/tests. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor Client
participant Ensemble as Ensemble API
participant Registry as ServiceRegistryService
participant Schemas as Zod Schemas
participant IPFS as IPFS (optional)
participant Contract as On‑chain Contract
participant Chain as Blockchain
Client->>Ensemble: registerService(params)
Ensemble->>Registry: registerService(params)
Registry->>Schemas: validateRegisterServiceParams(params)
Schemas-->>Registry: valid / throws ServiceValidationError
alt IPFS available
Registry->>IPFS: upload(metadata)
IPFS-->>Registry: ipfs://CID
Registry->>Contract: registerService(serviceUri=ipfs://CID, agent)
else fallback
Registry-->>Registry: build data URI
Registry->>Contract: registerService(serviceUri=data:..., agent)
end
Contract->>Chain: submit tx
Chain-->>Registry: tx receipt
Registry->>Registry: extractServiceIdFromReceipt(receipt)
Registry-->>Ensemble: ServiceRecord (on‑chain + metadata)
Ensemble-->>Client: ServiceRecord
note right of Registry: write ops require signer (requireSigner) → Service*Error classes on failure
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
Tip 🔌 Remote MCP (Model Context Protocol) integration is now available!Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats. ✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 10
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
packages/sdk/src/schemas/agent.schemas.ts (2)
35-55: Normalize Eliza params output type; make connectionUrl guaranteed post-transform.Currently both connectionUrl and websocketUrl are optional in the static type even after transform/refine, which is error-prone at call sites.
Apply:
-export const ElizaParamsSchema = z.object({ - connectionUrl: z.string().url('Invalid connection URL').optional(), - websocketUrl: z.string().url('Invalid websocket URL').optional(), // Deprecated, for backward compatibility - agentId: z.string().min(1, 'Agent ID is required'), - version: z.enum(['0.x', '1.x']), - env: z.enum(['production', 'dev']) -}).transform((data) => { - // Handle backward compatibility: migrate websocketUrl to connectionUrl - if (data.websocketUrl && !data.connectionUrl) { - const { websocketUrl, ...rest } = data; - return { ...rest, connectionUrl: websocketUrl }; - } - // Remove websocketUrl if connectionUrl exists - if (data.connectionUrl && data.websocketUrl) { - const { websocketUrl, ...rest } = data; - return rest; - } - return data; -}).refine((data) => data.connectionUrl, { - message: 'Either connectionUrl or websocketUrl (deprecated) is required' -}); +const ElizaRaw = z.object({ + connectionUrl: z.string().url('Invalid connection URL').optional(), + websocketUrl: z.string().url('Invalid websocket URL').optional(), // deprecated + agentId: z.string().min(1, 'Agent ID is required'), + version: z.enum(['0.x', '1.x']), + env: z.enum(['production', 'dev']) +}).superRefine((data, ctx) => { + if (!data.connectionUrl && !data.websocketUrl) { + ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Either connectionUrl or websocketUrl (deprecated) is required', path: ['connectionUrl'] }); + } +}); + +export const ElizaParamsSchema = ElizaRaw.transform(({ connectionUrl, websocketUrl, ...rest }) => ({ + ...rest, + connectionUrl: connectionUrl ?? websocketUrl!, // normalized, always present post-validate +}));
123-134: Agent metadata imageURI lacks validation; align with AgentRecord.imageURI is a plain string here but validated elsewhere; enforce the same URL/IPFS constraint.
-export const AgentMetadataSchema = z.object({ +export const AgentMetadataSchema = z.object({ name: z.string(), description: z.string(), - imageURI: z.string(), + imageURI: HttpOrIpfsUrlSchema, socials: AgentSocialsSchema, - agentCategory: z.string(), + agentCategory: z.string(),
🧹 Nitpick comments (12)
packages/sdk/src/errors.ts (1)
36-70: Solid addition of service-specific errors; consider a typed validation payload and a common base.
- Prefer typing validationErrors to Zod issues to improve DX.
- Optional: introduce a ServiceError base to unify catch logic and set prototype once.
Apply within-file diffs:
+import type { ZodIssue } from "zod"; @@ -export class ServiceValidationError extends Error { - constructor(message: string, public readonly validationErrors?: any) { +export class ServiceValidationError extends Error { + constructor(message: string, public readonly validationErrors?: readonly ZodIssue[]) { super(`Service validation failed: ${message}`); this.name = "ServiceValidationError"; } }Outside selected lines (example implementation for a base error):
// Consider placing near the top of the service error section export class ServiceError extends Error { constructor(message: string) { super(message); this.name = "ServiceError"; Object.setPrototypeOf(this, new.target.prototype); } } // Then extend ServiceError instead of Error for the V2 errorsIf you prefer not adding a base, at least add:
Object.setPrototypeOf(this, new.target.prototype);in each constructor to preserve instanceof across transpilation targets.packages/sdk/src/types.ts (1)
85-100: Add JSDoc@deprecatedto theServicealias inservice.schemas.tsIt looks like the
Servicealias is defined without a JSDoc deprecation tag, so IDEs (and downstream consumers) won’t automatically flag its use as deprecated. Please update the definition in
packages/sdk/src/schemas/service.schemas.tsaround line 209 to something like:/** * @deprecated Use `ServiceRecord` directly instead. * Legacy alias retained for backwards compatibility. */ export type Service = ServiceRecord;This will ensure that when you re-export
Service(as inpackages/sdk/src/types.ts), the deprecation notice carries through in IDEs and documentation tools..taskmaster/docs/prd.txt (1)
31-48: Clarify service identity and lookup semantics.Specify the canonical identifier used across SDK/CLI/contracts (e.g., serviceId UUID vs name), and whether getService() is by id or name. This avoids ambiguity when names collide and aligns schemas with CLI UX.
If the SDK currently exposes getService(name: string), consider adding getServiceById(id: string) and reflecting that here for consistency.
packages/sdk/src/schemas/agent.schemas.ts (2)
101-108: Deduplicate and harden URI validation via a shared schema.You repeat manual protocol checks for agentUri/imageURI. Prefer a shared HttpOrIpfsUrlSchema from base.schemas to ensure consistency and reuse.
After adding HttpOrIpfsUrlSchema in base.schemas.ts (see that file’s comment), use:
-import { - ethereumAddressRegex, - BigNumberishSchema, - EthereumAddressSchema, - URLSchema -} from './base.schemas'; +import { + ethereumAddressRegex, + BigNumberishSchema, + HttpOrIpfsUrlSchema +} from './base.schemas'; - agentUri: z.string().refine( - (val) => val.startsWith('http://') || val.startsWith('https://') || val.startsWith('ipfs://'), - 'URI must be a valid HTTP(S) URL or IPFS URI' - ), - imageURI: z.string().refine( - (val) => val.startsWith('http://') || val.startsWith('https://') || val.startsWith('ipfs://'), - 'Image URI must be a valid HTTP(S) URL or IPFS URI' - ), + agentUri: HttpOrIpfsUrlSchema, + imageURI: HttpOrIpfsUrlSchema,And similarly in RegisterAgentParamsSchema.imageURI.
Also applies to: 147-150
115-117: BigNumberish for reputation/totalRatings is too loose for counts.Counts/ratings typically require non-negative integers; BigNumberish (esp. strings) weakens validation.
- reputation: BigNumberishSchema, - totalRatings: BigNumberishSchema, + reputation: z.number().min(0), + totalRatings: z.number().int().min(0),If on-chain numeric strings are needed, consider a separate IntegerLike schema instead of the global BigNumberish (see base.schemas.ts comment).
packages/sdk/src/index.ts (1)
25-43: Missing semicolon after export block.Keep statement termination consistent; avoids diffs churn with formatters.
} from "./schemas/service.schemas" +;packages/sdk/src/schemas/service.schemas.ts (1)
91-95: endpointSchema naming is misleading.This field holds a URL, not a JSON Schema. Consider renaming to endpointUrl to avoid confusion.
No code change required if you prefer to keep the current name; just ensure docs clarify it.
packages/sdk/src/services/ServiceRegistryService.ts (5)
61-66: Doc return type mismatches (Service vs ServiceRecord).JSDoc says Promise, but the method returns ServiceRecord. Update for accuracy and IDE help.
- * @returns {Promise<Service>} The registered service with generated ID and metadata + * @returns {Promise<ServiceRecord>} The registered service with generated ID and metadataApply similarly to other methods returning ServiceRecord.
127-135: Post-registration validation is good. Consider parsing (not just safeParse) for strong guarantees.Current approach is fine; optionally use parseServiceRecord to throw with structured ZodError for uniform error handling.
- const serviceValidation = validateServiceRecord(serviceRecord); - if (!serviceValidation.success) { - throw new ServiceValidationError( - "Service validation failed after blockchain registration", - serviceValidation.error.issues - ); - } + // Strongly assert validity, let callers format via formatServiceValidationError if needed + parseServiceRecord(serviceRecord);
664-692: Legacy getService: avoid crypto.randomUUID portability issues.Global crypto.randomUUID is not universally available in Node. Prefer Node’s crypto import or a uuid lib.
- id: crypto.randomUUID(), // Placeholder - should come from contract + id: (globalThis.crypto?.randomUUID?.() ?? require('crypto').randomUUID?.() ?? Math.random().toString(36).slice(2)),Or import { randomUUID } from 'crypto' at top-level when targeting Node only.
138-144: Fragile revert detection; don’t rely on exact error.reason string.Contract revert reasons vary. Prefer matching error codes or parsing error data.
- if (error.reason === "Service already registered") { + if (typeof error?.reason === 'string' && /already\s+registered/i.test(error.reason)) { throw new ServiceAlreadyRegisteredError(parsedParams.name); }Longer-term: derive and match custom errors from the ABI where possible.
334-345: Console logging in SDKs can be noisy; inject a logger or use debug.Consider a pluggable logger or a noop-able debug channel to avoid polluting consumer logs.
No code diff required; optional ergonomics improvement.
Also applies to: 387-399, 481-485, 525-527
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (10)
.taskmaster/docs/prd.txt(1 hunks).taskmaster/tasks/tasks.json(1 hunks)packages/sdk/src/ensemble.ts(2 hunks)packages/sdk/src/errors.ts(1 hunks)packages/sdk/src/index.ts(2 hunks)packages/sdk/src/schemas/agent.schemas.ts(1 hunks)packages/sdk/src/schemas/base.schemas.ts(1 hunks)packages/sdk/src/schemas/service.schemas.ts(1 hunks)packages/sdk/src/services/ServiceRegistryService.ts(2 hunks)packages/sdk/src/types.ts(2 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
packages/sdk/src/ensemble.ts (2)
packages/sdk/src/schemas/service.schemas.ts (2)
RegisterServiceParams(205-205)Service(209-209)packages/sdk/src/types.ts (2)
RegisterServiceParams(91-91)Service(88-88)
packages/sdk/src/schemas/service.schemas.ts (2)
packages/sdk/src/schemas/base.schemas.ts (6)
BigNumberishSchema(21-25)ethereumAddressRegex(11-11)EthereumAddressSchema(49-52)OptionalDateTimeSchema(93-93)URLSchema(74-76)JsonSchema(31-40)packages/sdk/src/types.ts (12)
ServiceCategory(93-93)ServiceMethod(94-94)ServiceStatus(95-95)ServiceOperationalStatus(96-96)ServicePricingModel(97-97)ServicePricing(98-98)ServiceOnChain(89-89)ServiceMetadata(90-90)ServiceRecord(87-87)RegisterServiceParams(91-91)UpdateServiceParams(92-92)Service(88-88)
packages/sdk/src/services/ServiceRegistryService.ts (3)
packages/sdk/src/schemas/service.schemas.ts (9)
RegisterServiceParams(205-205)ServiceRecord(204-204)validateRegisterServiceParams(235-237)parseRegisterServiceParams(290-304)ServiceStatus(198-198)validateServiceRecord(220-222)UpdateServiceParams(206-206)validateUpdateServiceParams(244-246)parseUpdateServiceParams(311-325)packages/sdk/src/types.ts (4)
RegisterServiceParams(91-91)ServiceRecord(87-87)ServiceStatus(95-95)UpdateServiceParams(92-92)packages/sdk/src/errors.ts (6)
ServiceValidationError(58-63)ServiceAlreadyRegisteredError(22-27)ServiceStatusError(51-56)ServiceNotFoundError(37-42)ServiceAgentAssignmentError(65-70)ServiceOwnershipError(44-49)
🔇 Additional comments (7)
.taskmaster/tasks/tasks.json (1)
407-407: Metadata timestamp update: OK.No further action.
packages/sdk/src/ensemble.ts (1)
8-8: registerService signature migration: call sites verified, docs/examples need manual reviewAll existing call sites now use the single-object signature (
registerService(params: RegisterServiceParams)) and there are no remaining legacy positional calls. However, no documentation or example files referenceregisterService, so please confirm any README, markdown guides, or TS examples have been updated to reflect this change.• Call sites verified
– packages/sdk/test/ensemble.test.ts (lines 201, 218):await sdk.registerService(service)
– packages/sdk/scripts/register-services.ts (line 9):await ensemble.registerService(service)• SDK implementation
– packages/sdk/src/ensemble.ts (lines 273–276): new signature and delegation toServiceRegistryService.registerService(params)
– packages/sdk/src/services/ServiceRegistryService.ts (line 67): acceptsRegisterServiceParams
– LegacyregisterServiceLegacy()remains deprecated (lines 631–636)• Documentation check
– No matches for “registerService” in.md,.ts, or.tsxfiles outsidesrc/– please manually verify any external docs/examplesNext steps
- Manually review and update any user-facing documentation or sample code to use the new object-param signature.
- Add an
@remarksblock on theregisterServicemethod inpackages/sdk/src/ensemble.ts(around lines 273–276) to note the signature change and link to theRegisterServiceParamsschema for discoverability.packages/sdk/src/types.ts (1)
71-83: Agent type re-exports centralization looks good.No issues spotted.
packages/sdk/src/schemas/agent.schemas.ts (1)
9-10: No circular import detected between agent.schemas.ts and service.schemas.tsVerification steps confirmed that:
agent.schemas.tsre-exportsservice.schemas.ts;service.schemas.tsdoes not import or re-exportagent.schemas.ts;base.schemas.tshas no references to either schema;- both
packages/sdk/index.tsandpackages/sdk/src/index.tssimply re-export the schemas without introducing cycles.You can safely keep the barrel export as is.
packages/sdk/src/index.ts (1)
10-12: Good call exposing base schemas at the top-level.This helps downstreams reuse validators (addresses, UUID, URLs) without deep imports.
packages/sdk/src/schemas/service.schemas.ts (2)
128-145: Published-without-agent rule is great.This catches a common misconfiguration early.
387-394: Nice error formatting helper.User-facing formatting is helpful for CLI/SDK consumers.
| }, | ||
| { | ||
| "id": 25, | ||
| "title": "SDK: Implement Service Management System V2 with comprehensive CRUD operations, ownership model, and agent assignment capabilities", | ||
| "description": "Implement a comprehensive Service Management System V2 in the TypeScript SDK with full CRUD operations, ownership model, agent assignment capabilities, hybrid storage (on-chain + IPFS), and advanced discovery/filtering as specified in the PRD.", | ||
| "details": "1. ServiceRegistryService Enhancement:\n - Extend existing ServiceRegistryService class with V2 capabilities\n - Implement comprehensive CRUD operations: createService(), updateService(), deleteService(), getService(), listServices()\n - Add ownership model with transferOwnership(), getServiceOwner(), and ownership validation\n - Implement agent assignment/unassignment: assignAgent(), unassignAgent(), getAssignedAgents(), getServicesByAgent()\n - Add service lifecycle management with status tracking (draft, active, paused, archived, deleted)\n\n2. ServiceSchema Integration:\n - Create comprehensive ServiceSchema interface with required fields: id, name, description, category, owner, status, createdAt, updatedAt\n - Add optional fields: tags, metadata, pricing, requirements, capabilities, assignedAgents\n - Implement Zod validation schemas for all service operations\n - Support nested schema validation for complex service configurations\n\n3. Smart Contract Updates:\n - Extend ServiceRegistry.sol with V2 functionality including ownership transfers, agent assignments, and enhanced metadata\n - Implement hybrid storage pattern: core data on-chain (id, owner, status, assignedAgents) and metadata on IPFS\n - Add events for ServiceCreated, ServiceUpdated, ServiceDeleted, AgentAssigned, AgentUnassigned, OwnershipTransferred\n - Implement access control with onlyOwner modifiers and agent assignment permissions\n\n4. Hybrid Storage Implementation:\n - Integrate IPFS client for metadata storage using ipfs-http-client\n - Implement automatic IPFS pinning for service metadata and large data objects\n - Create metadata synchronization between on-chain references and IPFS content\n - Add content addressing and integrity verification for IPFS stored data\n\n5. Advanced Discovery and Filtering:\n - Implement advanced search with filters: category, status, owner, assignedAgent, tags, dateRange\n - Add pagination support with cursor-based navigation for large result sets\n - Implement sorting options: createdAt, updatedAt, name, category, status\n - Create full-text search capabilities for service names and descriptions\n - Add geolocation-based filtering if location metadata is available\n\n6. CLI Service Commands Integration:\n - Create 'ensemble services' command group with subcommands: create, list, get, update, delete, assign, unassign, transfer\n - Implement service-record.yaml configuration file support similar to agent-record.yaml\n - Add interactive service creation wizard with step-by-step guidance\n - Support bulk operations for service management and agent assignments", | ||
| "testStrategy": "1. Unit Testing:\n - Test all ServiceRegistryService methods with mocked blockchain interactions and IPFS operations\n - Validate ServiceSchema Zod validation with valid and invalid service data structures\n - Test ownership model operations including transfers and permission checks\n - Verify agent assignment/unassignment logic with various scenarios (single/multiple agents)\n - Test hybrid storage operations with mocked IPFS client responses\n\n2. Integration Testing:\n - Deploy updated smart contracts to testnet and test all V2 functionality end-to-end\n - Test IPFS integration with real IPFS nodes and verify metadata storage/retrieval\n - Validate service lifecycle management across different status transitions\n - Test advanced discovery and filtering with large datasets (1000+ services)\n - Verify CLI commands integration with real SDK operations\n\n3. Performance Testing:\n - Benchmark service creation/update operations with large metadata objects\n - Test pagination performance with datasets of varying sizes (100, 1K, 10K services)\n - Measure IPFS upload/download times for different metadata sizes\n - Test concurrent agent assignment operations and verify data consistency\n\n4. Security Testing:\n - Verify ownership validation prevents unauthorized service modifications\n - Test agent assignment permissions and access control mechanisms\n - Validate IPFS content integrity and prevent metadata tampering\n - Test smart contract upgrade scenarios and data migration safety\n\n5. End-to-End Testing:\n - Create complete service management workflows from CLI to blockchain\n - Test service discovery through various filtering combinations\n - Verify ownership transfers maintain data integrity and agent assignments\n - Test service deletion and cleanup of associated IPFS metadata", | ||
| "status": "pending", | ||
| "dependencies": [ | ||
| 1, | ||
| 3, | ||
| 21 | ||
| ], | ||
| "priority": "medium", | ||
| "subtasks": [ | ||
| { | ||
| "id": 1, | ||
| "title": "Extend ServiceRegistryService with V2 CRUD Operations and Ownership Model", | ||
| "description": "Enhance the existing ServiceRegistryService class with comprehensive CRUD operations, ownership model, and service lifecycle management capabilities.", | ||
| "dependencies": [], | ||
| "details": "Extend the ServiceRegistryService class to include: createService(), updateService(), deleteService(), getService(), listServices() methods with proper error handling and validation. Implement ownership model with transferOwnership(), getServiceOwner(), and ownership validation methods. Add service lifecycle management with status tracking (draft, active, paused, archived, deleted). Include proper TypeScript interfaces and error handling for all operations. Ensure backward compatibility with existing service registry functionality.", | ||
| "status": "done", | ||
| "testStrategy": "Unit tests for all CRUD operations with mocked blockchain interactions. Test ownership model operations including transfers and permission checks. Validate service lifecycle state transitions and status management." | ||
| }, | ||
| { | ||
| "id": 2, | ||
| "title": "Create ServiceSchema Interface and Zod Validation", | ||
| "description": "Design and implement comprehensive ServiceSchema interface with required and optional fields, along with Zod validation schemas for all service operations.", | ||
| "dependencies": [ | ||
| "25.1" | ||
| ], | ||
| "details": "Create ServiceSchema interface with required fields: id, name, description, category, owner, status, createdAt, updatedAt. Add optional fields: tags, metadata, pricing, requirements, capabilities, assignedAgents. Implement Zod validation schemas for service creation, updates, and queries. Support nested schema validation for complex service configurations. Create type-safe validation functions that can be used across the SDK. Include proper error messages and validation feedback for invalid schemas.", | ||
| "status": "done", | ||
| "testStrategy": "Validate ServiceSchema Zod validation with valid and invalid service data structures. Test nested schema validation for complex configurations. Verify type safety and error message clarity." | ||
| }, | ||
| { | ||
| "id": 3, | ||
| "title": "Update Smart Contracts with V2 Functionality and Hybrid Storage", | ||
| "description": "Extend ServiceRegistry.sol smart contract with V2 functionality including ownership transfers, agent assignments, and hybrid storage pattern implementation.", | ||
| "dependencies": [ | ||
| "25.2" | ||
| ], | ||
| "details": "Extend ServiceRegistry.sol with V2 functionality including ownership transfers, agent assignments, and enhanced metadata support. Implement hybrid storage pattern storing core data on-chain (id, owner, status, assignedAgents) and metadata on IPFS. Add events for ServiceCreated, ServiceUpdated, ServiceDeleted, AgentAssigned, AgentUnassigned, OwnershipTransferred. Implement access control with onlyOwner modifiers and agent assignment permissions. Include gas optimization and proper event indexing for efficient querying.", | ||
| "status": "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." | ||
| } | ||
| ] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
V2 task block looks comprehensive; add repo-level verification to ensure code parity.
Quickly validate that the SDK actually ships the listed V2 surface (methods, schemas, exports, CLI stubs). Run the script below from repo root.
Consider adding explicit acceptance criteria per subtask (inputs/outputs/preconditions) to tighten traceability from PRD → code → tests.
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== ServiceRegistryService V2 method presence =="
rg -nP --type=ts -C2 '(class\s+ServiceRegistryService\b)|\b(createService|updateService|deleteService|getService|listServices|assignAgent|unassignAgent|getAssignedAgents|getServicesByAgent|transferOwnership)\s*\(' packages | sed 's/^/ /'
echo "== Service V2 schemas and types =="
rg -nP --type=ts -n 'RegisterServiceParamsSchema|ServiceRecordSchema|ServiceMetadataSchema|ServiceOnChainSchema' packages/sdk/src/schemas/service.schemas.ts | sed 's/^/ /'
rg -nP --type=ts -n 'export\s+type\s+(RegisterServiceParams|ServiceRecord|Service\s*=|UpdateServiceParams|ServiceStatus|ServicePricing(Model)?)\b' packages/sdk/src/schemas/service.schemas.ts | sed 's/^/ /'
echo "== Barrel exports include service schemas =="
rg -nP --type=ts -n 'export .* from .*/schemas/service.schemas' packages/sdk/src/index.ts packages/sdk/src/types.ts | sed 's/^/ /'
echo "== New error types referenced by ServiceRegistryService =="
rg -nP --type=ts -n 'Service(NotFound|Ownership|Status|Validation|AgentAssignment)Error' packages | sed 's/^/ /'
echo "== CLI services command surface (if present) =="
rg -nP -n '\b(ensemble\s+services|\bservices\s+(create|list|get|update|delete|assign|unassign|transfer)\b)' packages | sed 's/^/ /'Length of output: 7700
Missing V2 Surface in SDK — Critical Implementations Required
The repo-level verification shows that several V2 features outlined in .taskmaster/tasks/tasks.json are not yet present in the SDK. Before merging, the following must be added:
• In packages/sdk/src/services/ServiceRegistryService.ts
– createService(...)
– assignAgent(...), unassignAgent(...)
– getAssignedAgents(...)
– transferOwnership(...)
• Barrel exports (e.g. packages/sdk/src/index.ts or packages/sdk/src/types.ts) must re-export the service schemas and types from schemas/service.schemas.ts.
• Define and throw the new error classes referenced in the V2 spec (e.g. ServiceNotFoundError, ServiceOwnershipError, ServiceStatusError, ServiceValidationError, ServiceAgentAssignmentError).
• Add CLI stubs and command group under ensemble services with the subcommands: create, list, get, update, delete, assign, unassign, transfer.
Optional refactor: consider enriching each subtask with explicit acceptance criteria (inputs, outputs, and preconditions) to strengthen traceability from PRD → code → tests.
🤖 Prompt for AI Agents
In .taskmaster/tasks/tasks.json around lines 334 to 402, the task outlines V2
SDK work but the SDK is missing the required surface; implement the following:
add createService(), assignAgent(), unassignAgent(), getAssignedAgents(),
transferOwnership() (and related CRUD/status methods if not present) to
packages/sdk/src/services/ServiceRegistryService.ts with proper validation and
error handling; add barrel exports in packages/sdk/src/index.ts (or types.ts) to
re-export service schemas and types from
packages/sdk/src/schemas/service.schemas.ts; create and export the new error
classes ServiceNotFoundError, ServiceOwnershipError, ServiceStatusError,
ServiceValidationError, ServiceAgentAssignmentError in the SDK error module and
update usages; and add CLI command stubs under the ensemble services group with
subcommands create, list, get, update, delete, assign, unassign, transfer so the
commands wire to the new service methods.
| export const BigNumberishSchema = z.union([ | ||
| z.bigint(), | ||
| z.string(), | ||
| z.number() | ||
| ]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
BigNumberishSchema accepts any string (validation bypass). Tighten to numeric/hex strings.
As written, "hello" passes, defeating schema guarantees wherever BigNumberish is used (e.g., pricing).
-export const BigNumberishSchema = z.union([
- z.bigint(),
- z.string(),
- z.number()
-]);
+export const BigNumberishSchema = z.union([
+ z.bigint(),
+ // Integers or decimals, or 0x-hex. Trim and forbid whitespace-only.
+ z.string()
+ .trim()
+ .regex(/^(0x[0-9a-fA-F]+|\d+(\.\d+)?)$/, 'Must be a numeric or 0x-hex string'),
+ z.number()
+]).refine((v) => v !== '' as any, { message: 'Value cannot be empty' });If you require integer-only semantics (ethers BigNumberish often does), swap the regex to /^(0x[0-9a-fA-F]+|\d+)$/.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export const BigNumberishSchema = z.union([ | |
| z.bigint(), | |
| z.string(), | |
| z.number() | |
| ]); | |
| export const BigNumberishSchema = z.union([ | |
| z.bigint(), | |
| // Integers or decimals, or 0x-hex. Trim and forbid whitespace-only. | |
| z.string() | |
| .trim() | |
| .regex(/^(0x[0-9a-fA-F]+|\d+(\.\d+)?)$/, 'Must be a numeric or 0x-hex string'), | |
| z.number() | |
| ]).refine((v) => v !== '' as any, { message: 'Value cannot be empty' }); |
🤖 Prompt for AI Agents
In packages/sdk/src/schemas/base.schemas.ts around lines 21 to 25,
BigNumberishSchema currently allows any string (e.g., "hello"); tighten
validation by restricting strings to numeric or hex formats and ensuring numbers
are integers: replace the z.string() branch with a string schema using the regex
/^(0x[0-9a-fA-F]+|\d+)$/ (or z.string().refine(...) with that pattern), and make
the number branch z.number().int() so only integer numbers are accepted; keep
z.bigint() as-is. Ensure the union becomes z.union([z.bigint(),
z.string().regex(/^(0x[0-9a-fA-F]+|\d+)$/), z.number().int()]).
| export const URLSchema = z.string().url({ | ||
| message: 'Must be a valid URL' | ||
| }); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Add HttpOrIpfsUrlSchema for reuse across SDK.
Centralize HTTP(S)/IPFS URI validation to reduce drift.
export const URLSchema = z.string().url({
message: 'Must be a valid URL'
});
+
+/**
+ * Schema for HTTP(S) or IPFS URLs (ipfs://CID or gateway links)
+ */
+export const HttpOrIpfsUrlSchema = z.string().refine(
+ (val) =>
+ typeof val === 'string' &&
+ (val.startsWith('http://') || val.startsWith('https://') || val.startsWith('ipfs://')),
+ 'Must be an HTTP(S) URL or ipfs:// URI'
+);Then consume in agent.schemas.ts and service.schemas.ts.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export const URLSchema = z.string().url({ | |
| message: 'Must be a valid URL' | |
| }); | |
| export const URLSchema = z.string().url({ | |
| message: 'Must be a valid URL' | |
| }); | |
| /** | |
| * Schema for HTTP(S) or IPFS URLs (ipfs://CID or gateway links) | |
| */ | |
| export const HttpOrIpfsUrlSchema = z.string().refine( | |
| (val) => | |
| typeof val === 'string' && | |
| (val.startsWith('http://') || | |
| val.startsWith('https://') || | |
| val.startsWith('ipfs://')), | |
| 'Must be an HTTP(S) URL or ipfs:// URI' | |
| ); |
🤖 Prompt for AI Agents
In packages/sdk/src/schemas/base.schemas.ts around lines 74-77, add and export a
reusable HttpOrIpfsUrlSchema that accepts either an http(s) URL or an ipfs://
URI (use z.union of the existing URLSchema and a string schema validating
ipfs://... with an appropriate regex/message); then update
packages/sdk/src/schemas/agent.schemas.ts and
packages/sdk/src/schemas/service.schemas.ts to import and use
HttpOrIpfsUrlSchema wherever HTTP or IPFS URIs are validated so the centralized
schema is reused across the SDK.
| 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 | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Enforce pricing-model invariants (free vs paid).
Currently price/tokenAddress are optional regardless of model. Add cross-field checks.
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
-});
+}).superRefine((data, ctx) => {
+ if (data.model === 'free') {
+ if (data.price) ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'price must be omitted for free model', path: ['price'] });
+ if (data.tokenAddress) ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'tokenAddress must be omitted for free model', path: ['tokenAddress'] });
+ } else {
+ if (data.price == null) ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'price is required for paid models', path: ['price'] });
+ if (!data.tokenAddress) ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'tokenAddress is required for paid models', path: ['tokenAddress'] });
+ }
+});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| 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 | |
| }); | |
| 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 | |
| }).superRefine((data, ctx) => { | |
| if (data.model === 'free') { | |
| if (data.price) { | |
| ctx.addIssue({ | |
| code: z.ZodIssueCode.custom, | |
| message: 'price must be omitted for free model', | |
| path: ['price'] | |
| }); | |
| } | |
| if (data.tokenAddress) { | |
| ctx.addIssue({ | |
| code: z.ZodIssueCode.custom, | |
| message: 'tokenAddress must be omitted for free model', | |
| path: ['tokenAddress'] | |
| }); | |
| } | |
| } else { | |
| if (data.price == null) { | |
| ctx.addIssue({ | |
| code: z.ZodIssueCode.custom, | |
| message: 'price is required for paid models', | |
| path: ['price'] | |
| }); | |
| } | |
| if (!data.tokenAddress) { | |
| ctx.addIssue({ | |
| code: z.ZodIssueCode.custom, | |
| message: 'tokenAddress is required for paid models', | |
| path: ['tokenAddress'] | |
| }); | |
| } | |
| } | |
| }); |
🤖 Prompt for AI Agents
In packages/sdk/src/schemas/service.schemas.ts around lines 54–59, the
ServicePricingSchema currently allows price and tokenAddress to be optional for
all models; add a cross-field validation using zod's refine/superRefine to
enforce invariants: when model indicates a PAID pricing model require price and
tokenAddress to be present (non-null/defined) and when model indicates FREE
ensure price and tokenAddress are absent/undefined (and freeQuota may be allowed
only for FREE if desired). Implement clear error messages per condition and
return the schema with the refinement to prevent invalid combinations.
| 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'), | ||
| status: ServiceStatusSchema, | ||
| version: z.number().int().min(0).describe('Version for cache invalidation'), | ||
| createdAt: OptionalDateTimeSchema, | ||
| updatedAt: OptionalDateTimeSchema | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Validate serviceUri scheme and require non-empty id.
ServiceOnChain.serviceUri should constrain to HTTP(S)/IPFS/data URIs to match how ServiceRegistryService constructs it; id should not be empty.
export const ServiceOnChainSchema = z.object({
- id: z.string().describe('Auto-incremented service ID from blockchain'),
+ id: z.string().min(1, 'Service ID is required').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'),
+ serviceUri: z.string().refine(
+ (v) => v.startsWith('ipfs://') || v.startsWith('data:') || v.startsWith('http://') || v.startsWith('https://'),
+ 'Service URI must be ipfs://, data:, or HTTP(S)'
+ ).describe('URI for service metadata'),
status: ServiceStatusSchema,
version: z.number().int().min(0).describe('Version for cache invalidation'),
createdAt: OptionalDateTimeSchema,
updatedAt: OptionalDateTimeSchema
});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| 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'), | |
| status: ServiceStatusSchema, | |
| version: z.number().int().min(0).describe('Version for cache invalidation'), | |
| createdAt: OptionalDateTimeSchema, | |
| updatedAt: OptionalDateTimeSchema | |
| }); | |
| export const ServiceOnChainSchema = z.object({ | |
| id: z.string().min(1, 'Service ID is required').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().refine( | |
| (v) => | |
| v.startsWith('ipfs://') || | |
| v.startsWith('data:') || | |
| v.startsWith('http://') || | |
| v.startsWith('https://'), | |
| 'Service URI must be ipfs://, data:, or HTTP(S)' | |
| ).describe('URI for service metadata'), | |
| status: ServiceStatusSchema, | |
| version: z.number().int().min(0).describe('Version for cache invalidation'), | |
| createdAt: OptionalDateTimeSchema, | |
| updatedAt: OptionalDateTimeSchema | |
| }); |
🤖 Prompt for AI Agents
In packages/sdk/src/schemas/service.schemas.ts around lines 70 to 80, the
ServiceOnChainSchema should require a non-empty id and restrict serviceUri
schemes; update the id validator to enforce non-empty (e.g., .min(1) or
equivalent) and change serviceUri to validate only http, https, ipfs and data
URI schemes by adding a regex or refine check that matches
^(https?:\/\/|ipfs:\/\/|data:).+ and returns a clear "Invalid serviceUri scheme"
message; keep all other fields unchanged.
| // Fallback for testing without IPFS - store as data URI | ||
| serviceUri = `data:application/json;base64,${Buffer.from(JSON.stringify(parsedParams.metadata)).toString('base64')}`; | ||
| } | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Buffer in browser environments; add cross-platform base64 encoding.
SDKs often run in browsers where Buffer may be undefined. Provide a safe fallback.
- serviceUri = `data:application/json;base64,${Buffer.from(JSON.stringify(parsedParams.metadata)).toString('base64')}`;
+ const json = JSON.stringify(parsedParams.metadata);
+ const toBase64 = () => {
+ if (typeof Buffer !== 'undefined') return Buffer.from(json, 'utf-8').toString('base64');
+ // Browser-safe UTF-8 → base64
+ const bytes = new TextEncoder().encode(json);
+ let bin = '';
+ for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
+ return btoa(bin);
+ };
+ serviceUri = `data:application/json;base64,${toBase64()}`;🤖 Prompt for AI Agents
In packages/sdk/src/services/ServiceRegistryService.ts around lines 92 to 95,
replace the direct use of Buffer to build the data URI with a cross-platform
base64 encoder: add a small helper that takes a string and returns base64 by
using Buffer.from(str).toString('base64') when Buffer/globalThis.Buffer exists,
otherwise use the browser-safe btoa(unescape(encodeURIComponent(str))) (or
globalThis.btoa) fallback; then use that helper to encode
JSON.stringify(parsedParams.metadata) and construct the
data:application/json;base64, URI so the code works in both Node and browser
environments.
| // 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 | ||
| ); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Status updates via updateService bypass transition checks.
If updates include status, validate the transition against currentService.status.
- const updatedService: ServiceRecord = {
+ if (parsedUpdates.status && parsedUpdates.status !== currentService.status) {
+ this.validateStatusTransition(serviceId, currentService.status, parsedUpdates.status);
+ }
+ 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()
};Also applies to: 176-183
| if (service.status === 'published') { | ||
| throw new ServiceStatusError( | ||
| serviceId, | ||
| service.status, | ||
| 'inactive or archived' | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Deletion policy conflicts with transition table; make them consistent and include serviceId in errors.
deleteService forbids deletion from 'published', but validateStatusTransition currently allows it. Also, ServiceStatusError is constructed with "unknown" serviceId.
- private validateStatusTransition(currentStatus: ServiceStatus, newStatus: ServiceStatus): void {
+ private validateStatusTransition(serviceId: string, currentStatus: ServiceStatus, newStatus: ServiceStatus): void {
const validTransitions: Record<ServiceStatus, ServiceStatus[]> = {
- 'draft': ['published', 'deleted'],
- 'published': ['archived', 'deleted'],
+ 'draft': ['published', 'deleted'],
+ 'published': ['archived'], // forbid direct delete from published
'archived': ['published', 'deleted'], // Allow reactivation from archive
'deleted': [] // No transitions from deleted state
};
const allowedTransitions = validTransitions[currentStatus] || [];
if (!allowedTransitions.includes(newStatus)) {
- throw new ServiceStatusError(
- "unknown",
- currentStatus,
- `Cannot transition from ${currentStatus} to ${newStatus}. Allowed: ${allowedTransitions.join(', ')}`
- );
+ throw new ServiceStatusError(serviceId, currentStatus, allowedTransitions.join(' or ') || 'n/a');
}
}And adjust callers:
- this.validateStatusTransition(service.status, status);
+ this.validateStatusTransition(serviceId, service.status, status);deleteService then aligns with the table (published → archived first).
Also applies to: 607-623, 374-383
🤖 Prompt for AI Agents
In packages/sdk/src/services/ServiceRegistryService.ts around lines 233-238 (and
also check 374-383, 607-623): the deleteService logic currently forbids deleting
a service in 'published' state but validateStatusTransition allows
published→deleted; also ServiceStatusError instances are being constructed with
an "unknown" serviceId. Update the status transition rules so they match the
deletion policy (disallow published→deleted; require
published→archived→deleted), and change all ServiceStatusError constructions in
the listed ranges to pass the real serviceId instead of "unknown"; ensure
deleteService enforces published→archived first and callers reflect the same
transition validation.
| async getServiceById(serviceId: string): Promise<ServiceRecord> { | ||
| 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; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
getServiceById is unimplemented but widely depended upon.
All management operations will throw. At minimum, fail fast with a clear NotImplemented error in the public methods that depend on it to avoid surprising partial state (e.g., after IPFS upload).
- // 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");
+ console.log(`Getting service by ID: ${serviceId}`);
+ throw new Error("NotImplemented: getServiceById requires ServiceRegistry V2 support");Also consider guarding update/delete/transfer/status/assign/unassign with a runtime check and a uniform NotImplemented error until read APIs exist.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| async getServiceById(serviceId: string): Promise<ServiceRecord> { | |
| 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; | |
| } | |
| } | |
| async getServiceById(serviceId: string): Promise<ServiceRecord> { | |
| try { | |
| console.log(`Getting service by ID: ${serviceId}`); | |
| throw new Error("NotImplemented: getServiceById requires ServiceRegistry V2 support"); | |
| } 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; | |
| } | |
| } |
🤖 Prompt for AI Agents
In packages/sdk/src/services/ServiceRegistryService.ts around lines 260-277, the
getServiceById placeholder currently throws a generic Error and logs, which
causes surprising partial state for callers; replace the generic throw with a
clear, exported NotImplemented/ServiceNotImplementedError (e.g.,
ServiceNotImplementedError or NotImplementedError) and ensure the method
immediately throws that error (no fallback behavior), update the catch so it
only maps actual "not found" messages to ServiceNotFoundError and rethrows other
errors unchanged, and add a runtime guard at the start of all public mutating
methods (update/delete/transfer/status/assign/unassign) to check for this
unimplemented read capability and throw the same
NotImplemented/ServiceNotImplementedError until real read APIs exist.
| 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) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
🧩 Analysis chain
Use getEventTopic for reliability with ethers v6; parse by topic string.
EventFragment.topicHash availability varies. getEventTopic('ServiceRegistered') is the stable API.
- const serviceRegisteredTopic = this.serviceRegistry.interface.getEvent('ServiceRegistered').topicHash;
- const event = receipt.logs.find((log: any) => log.topics[0] === serviceRegisteredTopic);
+ const topic = this.serviceRegistry.interface.getEventTopic('ServiceRegistered');
+ const event = receipt.logs.find((log: any) => log.topics?.[0] === topic);🏁 Script executed:
#!/bin/bash
# Verify ethers major version and ensure Interface.getEventTopic exists in your installed version
rg -nP '"ethers"\s*:\s*"' -g "**/package.json" -C2
rg -nP "getEventTopic\(" -g "node_modules/**/ethers*/**/*.js" -S || trueLength of output: 820
🏁 Script executed:
#!/bin/bash
# Search for getEventTopic and topicHash in the installed ethers v6 package
rg -n "getEventTopic" -C3 -g "node_modules/ethers/**/*"
rg -n "topicHash" -C3 -g "node_modules/ethers/**/*"Length of output: 405
🏁 Script executed:
#!/bin/bash
# Broad search for getEventTopic and topicHash in node_modules
rg --no-ignore -n "getEventTopic" -C3 node_modules
rg --no-ignore -n "topicHash" -C3 node_modulesLength of output: 203
Use interface.getEventTopic for a stable topic hash lookup
Ethers v6’s Interface.getEventTopic is the documented, public API for obtaining an event’s topic hash. While EventFragment.topicHash currently exists, using getEventTopic directly avoids coupling to the fragment’s internal properties and reads more clearly. (docs.ethers.org, sambacha.github.io)
• Replace the two-step lookup via getEvent(...).topicHash with a single call to getEventTopic
• Add optional chaining on log.topics to guard against empty or malformed logs
- const serviceRegisteredTopic = this.serviceRegistry.interface.getEvent('ServiceRegistered').topicHash;
- const event = receipt.logs.find((log: any) => log.topics[0] === serviceRegisteredTopic);
+ const topic = this.serviceRegistry.interface.getEventTopic('ServiceRegistered');
+ const event = receipt.logs.find((log: any) => log.topics?.[0] === topic);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| 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) { | |
| private extractServiceIdFromReceipt(receipt: any): string { | |
| try { | |
| // Look for ServiceRegistered event in the logs | |
| const topic = this.serviceRegistry.interface.getEventTopic('ServiceRegistered'); | |
| const event = receipt.logs.find((log: any) => log.topics?.[0] === topic); | |
| if (event) { | |
| const parsed = this.serviceRegistry.interface.parseLog(event); | |
| if (parsed && parsed.args && parsed.args.serviceId) { |
🤖 Prompt for AI Agents
In packages/sdk/src/services/ServiceRegistryService.ts around lines 558 to 566,
replace the current two-step event topic lookup that uses
this.serviceRegistry.interface.getEvent(...).topicHash with a single call to
this.serviceRegistry.interface.getEventTopic('ServiceRegistered'), and update
the logs search to guard against malformed logs by using optional chaining
(e.g., log.topics?.[0]) so the find won't throw on empty or undefined topics;
keep the parsed log logic the same after these changes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
CLAUDE.md (1)
170-175: Close the code fence; headings currently render as code.The code block under “Always Do This” isn’t closed, so subsequent markdown (including “Project Documentation”) renders inside the code block.
```bash # ✅ 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..." +```
🧹 Nitpick comments (11)
CLAUDE.md (1)
67-73: Avoid hard-coded absolute paths; use a placeholder or command to detect repo root.Docs currently point to a user-specific path, which will confuse other devs. Prefer or a discoverable command.
- - Task-master is initialized at: `/Users/leon/workspace/ensemble/ensemble-framework/` + - Task-master is initialized at: `<project-root>` (e.g., `$(git rev-parse --show-toplevel)`)If you want, I can sweep the file to replace all absolute paths with
<project-root>.Also applies to: 75-83, 100-106, 126-137, 170-173
.taskmaster/docs/prd.txt (3)
161-166: Doc/API mismatch: getService takes serviceId here, but SDK uses name. Align the contract.Current SDK method is
getService(name: string): Promise<ServiceRecord>; PRD showsgetService(serviceId). Prefer ID lookup for uniqueness, or document both.Proposed doc tweak if keeping current SDK:
- - **ServiceRegistryService.getService(serviceId)**: Get complete service record + - **ServiceRegistryService.getServiceByName(name)**: Get complete service record by name + - **ServiceRegistryService.getServiceById(serviceId)**: Get complete service record by ID (preferred, unique)Do you want a follow-up PR to add
getServiceByIdto the SDK and mark the name-based variant as legacy?
178-182: Activation precondition is good; consider stating the exact validation error.Add a short, actionable error message in the spec to keep SDK/CLI consistent.
- - Validation: Ensures agent is assigned before activation + - Validation: Ensures agent is assigned before activation + - Error: "ServiceStatusError: Agent must be assigned before publishing"
183-201: Avoid hardcoded field counts; they drift quickly.“8 fields”, “9+ fields” will bit-rot as schemas evolve. Remove counts or move them to a generated appendix.
- - ServiceOnChainSchema: Minimal blockchain data (8 fields) - - ServiceMetadataSchema: Rich IPFS metadata (9+ fields including operational status) + - ServiceOnChainSchema: Minimal blockchain data + - ServiceMetadataSchema: Rich IPFS metadata (including operational status)packages/sdk/test/ensemble.test.ts (2)
193-203: Type the service input to catch shape regressions at compile time.Annotate with RegisterServiceParams for stronger constraints.
- const service = { + import type { RegisterServiceParams } from "../src/types"; + const service: RegisterServiceParams = { name: "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: {} } };
205-216: Type the mock return and assert the call.Use ServiceRecord for the mock and verify the service was called with the expected payload.
- const mockServiceRecord = { + import type { ServiceRecord } from "../src/types"; + const mockServiceRecord: ServiceRecord = { 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 }; @@ const response = await sdk.registerService(service); expect(response).toEqual(mockServiceRecord); + expect(serviceRegistryService.registerService).toHaveBeenCalledWith(service);Also applies to: 218-223
packages/sdk/src/ensemble.ts (5)
269-276: registerService now returns ServiceRecord; good. Consider pre-validating params for earlier feedback.If ServiceRegistryService already validates via Zod, ignore; otherwise, validate here to fail fast.
async registerService(params: RegisterServiceParams): Promise<ServiceRecord> { this.requireSigner(); - return this.serviceRegistryService.registerService(params); + return this.serviceRegistryService.registerService(params); }Option: add
validateRegisterServiceParams(params)before forwarding.
281-285: Name-based getService conflicts with PRD’s ID-based doc; suggest adding getServiceById and deprecating name lookup.Name collisions are likely in a marketplace. Keep this for DX, but add an ID-based variant.
Proposed follow-up: add
getServiceById(id: string): Promise<ServiceRecord>to Ensemble and ServiceRegistryService, and update docs/tests accordingly. I can open an issue and draft the change. Do you want that?
62-71: Provider fallback ignores configured rpcUrl.When no signer/provider is supplied, prefer config.network.rpcUrl over default JsonRpcProvider.
- const provider = signerOrProvider || new ethers.JsonRpcProvider(); + const provider = + signerOrProvider || + new ethers.JsonRpcProvider( + (config as any)?.network?.rpcUrl ?? undefined + );
46-50: Propagate signer to all write-capable services.You update agents and serviceRegistry services; if TaskService ever performs writes directly, consider a
setSignerthere too. If not applicable, ignore.
224-230: Minor JSDoc param name mismatch.Doc says agentAddress; param is agentId. Align naming for clarity.
- * @param {string} agentAddress - The address of the agent. + * @param {string} agentId - The address of the agent.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (5)
.taskmaster/docs/prd.txt(2 hunks)CLAUDE.md(1 hunks)packages/sdk/scripts/data/servicesList.ts(1 hunks)packages/sdk/src/ensemble.ts(2 hunks)packages/sdk/test/ensemble.test.ts(2 hunks)
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-08-01T07:27:26.601Z
Learnt from: CR
PR: ensemble-codes/ensemble-framework#0
File: packages/contracts/.cursor/rules/project-intelligence.mdc:0-0
Timestamp: 2025-08-01T07:27:26.601Z
Learning: Applies to packages/contracts/test/**/*{Integration,EnsembleCredits,Registry}*.sol : Integration testing must verify interaction between EnsembleCredits and registry contracts
Applied to files:
packages/sdk/test/ensemble.test.ts
📚 Learning: 2025-08-26T05:38:59.229Z
Learnt from: CR
PR: ensemble-codes/ensemble-framework#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-26T05:38:59.229Z
Learning: All task-master operations must be run from the project root directory
Applied to files:
CLAUDE.md
📚 Learning: 2025-08-26T05:38:59.229Z
Learnt from: CR
PR: ensemble-codes/ensemble-framework#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-26T05:38:59.229Z
Learning: When creating or managing tasks, always cd to the project root first
Applied to files:
CLAUDE.md
📚 Learning: 2025-08-26T05:38:59.229Z
Learnt from: CR
PR: ensemble-codes/ensemble-framework#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-26T05:38:59.229Z
Learning: Use task-master commands (list, next, show, expand, add-subtask, set-status) only from the project root
Applied to files:
CLAUDE.md
🧬 Code graph analysis (2)
packages/sdk/scripts/data/servicesList.ts (1)
packages/sdk/src/types.ts (1)
RegisterServiceParams(91-91)
packages/sdk/src/ensemble.ts (2)
packages/sdk/src/types.ts (2)
RegisterServiceParams(91-91)ServiceRecord(87-87)packages/sdk/src/schemas/service.schemas.ts (2)
RegisterServiceParams(205-205)ServiceRecord(204-204)
🪛 LanguageTool
.taskmaster/docs/prd.txt
[grammar] ~183-~183: There might be a mistake here.
Context: ...chema architecture and validation system - Three-Schema Architecture: Clear separ...
(QB_NEW_EN)
[grammar] ~184-~184: There might be a mistake here.
Context: ...tion of concerns for optimal performance - ServiceOnChainSchema: Minimal blockchain...
(QB_NEW_EN)
[grammar] ~185-~185: There might be a mistake here.
Context: ...hema: Minimal blockchain data (8 fields) - ServiceMetadataSchema: Rich IPFS metadat...
(QB_NEW_EN)
[grammar] ~186-~186: There might be a mistake here.
Context: ...(9+ fields including operational status) - ServiceRecordSchema: Complete combined v...
(QB_NEW_EN)
[grammar] ~189-~189: There might be a mistake here.
Context: ...-state model aligned with business needs - draft: Service being configured, not rea...
(QB_NEW_EN)
[grammar] ~190-~190: There might be a mistake here.
Context: ...vice being configured, not ready for use - published: Service available for discove...
(QB_NEW_EN)
[grammar] ~191-~191: There might be a mistake here.
Context: ...ce available for discovery and execution - archived: Service deprecated but preserv...
(QB_NEW_EN)
[grammar] ~192-~192: There might be a mistake here.
Context: ...ice deprecated but preserved for history - deleted: Service removed (soft delete fo...
(QB_NEW_EN)
[grammar] ~195-~195: There might be a mistake here.
Context: ...validation with enhanced error reporting - validateServiceRecord(): Complete servic...
(QB_NEW_EN)
[grammar] ~196-~196: There might be a mistake here.
Context: ...iceRecord(): Complete service validation - validateServiceOnChain(): On-chain data ...
(QB_NEW_EN)
[grammar] ~197-~197: There might be a mistake here.
Context: ...rviceOnChain(): On-chain data validation - validateServiceMetadata(): IPFS metadata...
(QB_NEW_EN)
[grammar] ~198-~198: There might be a mistake here.
Context: ...viceMetadata(): IPFS metadata validation - validateRegisterServiceParams(): Registr...
(QB_NEW_EN)
[grammar] ~199-~199: There might be a mistake here.
Context: ...ams(): Registration parameter validation - validateUpdateServiceParams(): Update pa...
(QB_NEW_EN)
[grammar] ~202-~202: There might be a mistake here.
Context: ...er Experience**: Full TypeScript support - ServiceRecord: Primary type for SDK cons...
(QB_NEW_EN)
[grammar] ~203-~203: There might be a mistake here.
Context: ...ceRecord: Primary type for SDK consumers - ServiceOnChain: Blockchain data type ...
(QB_NEW_EN)
[grammar] ~204-~204: There might be a mistake here.
Context: ... - ServiceOnChain: Blockchain data type - ServiceMetadata: IPFS metadata type ...
(QB_NEW_EN)
[grammar] ~205-~205: There might be a mistake here.
Context: ... - ServiceMetadata: IPFS metadata type - RegisterServiceParams: Registration inpu...
(QB_NEW_EN)
[grammar] ~206-~206: There might be a mistake here.
Context: ...erServiceParams: Registration input type - UpdateServiceParams: Update input type w...
(QB_NEW_EN)
[grammar] ~209-~209: There might be a mistake here.
Context: ...**: Legacy aliases and migration support - Service type alias → ServiceRecord -...
(QB_NEW_EN)
[grammar] ~210-~210: There might be a mistake here.
Context: ... - Service type alias → ServiceRecord - Deprecated function aliases with clear m...
(QB_NEW_EN)
[grammar] ~211-~211: There might be a mistake here.
Context: ...nction aliases with clear migration path - Gradual migration strategy for existing ...
(QB_NEW_EN)
CLAUDE.md
[grammar] ~178-~178: There might be a mistake here.
Context: ...ect Documentation ### PRD File Location The Project Requirements Document (PRD) ...
(QB_NEW_EN)
🔇 Additional comments (6)
CLAUDE.md (1)
176-181: PRD pointer addition LGTM.Clear, actionable, and aligned with our learnings about running task-master from project root.
packages/sdk/scripts/data/servicesList.ts (2)
1-4: Type migration to RegisterServiceParams[] looks correct.Import and exported type align with the V2 schemas.
6-13: I've added scripts to:
- Locate and inspect the
ServiceMetadataSchemadefinition and itsendpointSchemafield.- Confirm the presence and form of
ServiceMethodandServiceCategoryin the types.Once we have these outputs, we can accurately determine whether to rename
endpointSchemaor adjust the type/schema alignment and finalize the refactoring recommendation.packages/sdk/test/ensemble.test.ts (2)
225-260: Duplicate-registration path looks good.Resolved then rejected sequencing correctly simulates “first ok, second dup”.
525-541: Signer guard test for registerService is solid.Confirms the new requireSigner path.
packages/sdk/src/ensemble.ts (1)
8-13: Type surface changes LGTM.Importing RegisterServiceParams and returning ServiceRecord aligns with V2.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 7
♻️ Duplicate comments (7)
packages/sdk/src/services/ServiceRegistryService.ts (5)
606-607: Use interface.getEventTopic for better compatibility with ethers v6.Apply this diff to use the stable API:
- const serviceRegisteredTopic = this.serviceRegistry.interface.getEvent('ServiceRegistered').topicHash; - const event = receipt.logs.find((log: any) => log.topics[0] === serviceRegisteredTopic); + const topic = this.serviceRegistry.interface.getEventTopic('ServiceRegistered'); + const event = receipt.logs.find((log: any) => log.topics?.[0] === topic);
101-101: Cross-platform base64 encoding needed for browser compatibility.The direct use of Node.js
Bufferwill fail in browser environments. Implement a cross-platform solution for base64 encoding.Apply this diff to add browser-safe base64 encoding:
- serviceUri = `data:application/json;base64,${Buffer.from(JSON.stringify(metadataWithTimestamps)).toString('base64')}`; + const json = JSON.stringify(metadataWithTimestamps); + const toBase64 = () => { + if (typeof Buffer !== 'undefined') { + return Buffer.from(json, 'utf-8').toString('base64'); + } + // Browser-safe UTF-8 → base64 + const bytes = new TextEncoder().encode(json); + let bin = ''; + for (let i = 0; i < bytes.length; i++) { + bin += String.fromCharCode(bytes[i]); + } + return btoa(bin); + }; + serviceUri = `data:application/json;base64,${toBase64()}`;This same pattern should be applied to line 233 as well.
163-172: Status updates via updateService bypass transition validation.When the update includes a status change, the transition isn't validated against the current status, potentially violating lifecycle rules.
Apply this diff to validate status transitions:
const parsedUpdates = parseUpdateServiceParams({ ...updates, id: serviceId }); try { // Get current service const currentService = await this.getServiceById(serviceId); // Check ownership await this.verifyServiceOwnership(currentService, await this.signer!.getAddress()); + + // Validate status transition if status is being updated + if (parsedUpdates.status && parsedUpdates.status !== currentService.status) { + this.validateStatusTransition(serviceId, currentService.status, parsedUpdates.status); + }
298-315: getServiceById is unimplemented, causing all management operations to fail.The placeholder implementation throws an error, which makes all operations that depend on it (update, delete, transfer, status changes, agent management) unusable. This should be clearly marked as not yet supported.
Consider adding clear NotImplementedError handling and documentation:
+export class ServiceNotImplementedError extends Error { + constructor(feature: string) { + super(`Feature not yet implemented: ${feature} requires ServiceRegistry V2 contract support`); + this.name = "ServiceNotImplementedError"; + } +} async getServiceById(serviceId: string): Promise<ServiceRecord> { 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"); + throw new ServiceNotImplementedError("getServiceById"); } catch (error: any) { console.error(`Error getting service ${serviceId}:`, error); - if (error.message.includes("not found") || error.message.includes("does not exist")) { + if (error instanceof ServiceNotImplementedError) { + throw error; + } else if (error.message.includes("not found") || error.message.includes("does not exist")) { throw new ServiceNotFoundError(serviceId); } throw error; } }Also consider guarding dependent methods to provide a better developer experience until the read API is implemented.
652-668: Status transition validation inconsistent with deletion logic.The validateStatusTransition allows 'published' → 'deleted', but deleteService at line 271-276 forbids deletion of published services. Additionally, ServiceStatusError is constructed with "unknown" serviceId instead of the actual ID.
Apply this diff to fix the inconsistency:
- private validateStatusTransition(currentStatus: ServiceStatus, newStatus: ServiceStatus): void { + private validateStatusTransition(serviceId: string, currentStatus: ServiceStatus, newStatus: ServiceStatus): void { const validTransitions: Record<ServiceStatus, ServiceStatus[]> = { 'draft': ['published', 'deleted'], - 'published': ['archived', 'deleted'], + 'published': ['archived'], // Require archiving before deletion 'archived': ['published', 'deleted'], // Allow reactivation from archive 'deleted': [] // No transitions from deleted state }; const allowedTransitions = validTransitions[currentStatus] || []; if (!allowedTransitions.includes(newStatus)) { throw new ServiceStatusError( - "unknown", + serviceId, currentStatus, - `Cannot transition from ${currentStatus} to ${newStatus}. Allowed: ${allowedTransitions.join(', ')}` + allowedTransitions.join(' or ') || 'none' ); } }Also update the caller at line 423:
- this.validateStatusTransition(service.status, status); + this.validateStatusTransition(serviceId, service.status, status);packages/sdk/src/schemas/service.schemas.ts (2)
54-59: Add pricing model invariants validation.The schema allows optional price/tokenAddress for all models, but paid models should require these fields while free models should forbid them.
Apply this refinement to enforce pricing invariants:
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 -}); +}).refine( + (data) => { + if (data.model === 'free') { + return !data.price && !data.tokenAddress; + } else { + return data.price !== undefined && data.tokenAddress !== undefined; + } + }, + (data) => ({ + message: data.model === 'free' + ? 'Free services should not have price or tokenAddress' + : 'Paid services require both price and tokenAddress' + }) +);
70-77: ServiceOnChain schema needs stricter validation.The
idfield should enforce non-empty values, andserviceUrishould validate URI schemes to match the expected formats (IPFS, data URIs, HTTP(S)).Apply this diff to add proper validation:
export const ServiceOnChainSchema = z.object({ - id: z.string().describe('Auto-incremented service ID from blockchain'), + id: z.string().min(1, 'Service ID is required').describe('Auto-incremented service ID from blockchain'), owner: EthereumAddressSchema, agentAddress: z.string().regex(ethereumAddressRegex, 'Invalid agent address format').optional(), - serviceUri: z.string().describe('IPFS URI for service metadata'), + serviceUri: z.string().refine( + (v) => v.startsWith('ipfs://') || v.startsWith('data:') || v.startsWith('http://') || v.startsWith('https://'), + 'Service URI must be ipfs://, data:, or HTTP(S)' + ).describe('URI for service metadata'), status: ServiceStatusSchema, version: z.number().int().min(0).describe('Version for cache invalidation') });
🧹 Nitpick comments (3)
packages/sdk/src/services/ServiceRegistryService.ts (1)
539-542: Status validation for unassignAgentFromService is too restrictive.The validation only allows 'draft' or 'archived' status, but the contract expects numeric values (0 or 2). Consider using an enum or constants for clarity.
Consider defining status constants for better maintainability:
+// Add at the top of the class +private readonly STATUS_MAPPING = { + 'draft': 0, + 'published': 1, + 'archived': 2, + 'deleted': 3 +} as const; // Validate status parameter if (newStatus !== 'draft' && newStatus !== 'archived') { throw new ServiceValidationError('Status must be either "draft" or "archived"'); } // Later in the code (line 564): - newStatus === 'draft' ? 0 : 2 // DRAFT = 0, ARCHIVED = 2 + this.STATUS_MAPPING[newStatus]packages/contracts/contracts/ServiceRegistryUpgradeable.sol (2)
338-347: Consider gas optimization for array removal operations.The
_removeFromOwnerServicesand_removeFromAgentServicesfunctions use linear search which can be expensive for large arrays. Consider tracking indices or using a different data structure for O(1) removal.For frequently modified arrays, consider maintaining a mapping of serviceId to array index:
mapping(address => mapping(uint256 => uint256)) private ownerServiceIndex; mapping(address => mapping(uint256 => uint256)) private agentServiceIndex;This would allow O(1) removal at the cost of additional storage for the index mappings. The trade-off is worthwhile for users with many services.
Also applies to: 354-363
59-61: Initialization values are redundant.Setting
nextServiceIdandtotalServicesto 0 is unnecessary as storage variables default to 0.function initialize() public initializer { __Ownable_init(msg.sender); __UUPSUpgradeable_init(); - nextServiceId = 0; - totalServices = 0; }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (9)
.taskmaster/docs/prd.txt(2 hunks).taskmaster/tasks/tasks.json(1 hunks)packages/contracts/contracts/AgentsRegistryUpgradeable.sol(4 hunks)packages/contracts/contracts/ServiceRegistryUpgradeable.sol(2 hunks)packages/sdk/src/abi/ServiceRegistry.json(1 hunks)packages/sdk/src/ensemble.ts(5 hunks)packages/sdk/src/schemas/service.schemas.ts(1 hunks)packages/sdk/src/services/ServiceRegistryService.ts(2 hunks)packages/sdk/test/ensemble.test.ts(4 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
packages/contracts/contracts/**/*.sol
📄 CodeRabbit inference engine (packages/contracts/.cursor/rules/project-intelligence.mdc)
packages/contracts/contracts/**/*.sol: All public functions in Solidity contracts must have complete NatSpec documentation comments
Custom errors with descriptive names should be used for error handling in Solidity contracts instead of string messages
All state changes in Solidity contracts must emit appropriate events
Follow OpenZeppelin security patterns in all Solidity contracts
Files:
packages/contracts/contracts/AgentsRegistryUpgradeable.solpackages/contracts/contracts/ServiceRegistryUpgradeable.sol
packages/contracts/**/*.sol
📄 CodeRabbit inference engine (packages/contracts/.cursor/rules/role.mdc)
packages/contracts/**/*.sol: Use explicit function visibility modifiers and appropriate natspec comments.
Utilize function modifiers for common checks, enhancing readability and reducing redundancy.
Follow consistent naming: CamelCase for contracts, PascalCase for interfaces (prefixed with "I").
Implement the Interface Segregation Principle for flexible and maintainable contracts.
Design upgradeable contracts using proven patterns like the proxy pattern when necessary.
Implement comprehensive events for all significant state changes.
Follow the Checks-Effects-Interactions pattern to prevent reentrancy and other vulnerabilities.
Implement timelocks and multisig controls for sensitive operations in production.
Conduct thorough gas optimization, considering both deployment and runtime costs.
Use OpenZeppelin's AccessControl for fine-grained permissions.
Use Solidity 0.8.0+ for built-in overflow/underflow protection.
Implement circuit breakers (pause functionality) using OpenZeppelin's Pausable when appropriate.
Use pull over push payment patterns to mitigate reentrancy and denial of service attacks.
Implement rate limiting for sensitive functions to prevent abuse.
Use OpenZeppelin's SafeERC20 for interacting with ERC20 tokens.
Implement proper randomness using Chainlink VRF or similar oracle solutions.
Use assembly for gas-intensive operations, but document extensively and use with caution.
Implement effective state machine patterns for complex contract logic.
Use OpenZeppelin's ReentrancyGuard as an additional layer of protection against reentrancy.
Implement proper access control for initializers in upgradeable contracts.
Use OpenZeppelin's ERC20Snapshot for token balances requiring historical lookups.
Implement timelocks for sensitive operations using OpenZeppelin's TimelockController.
Use OpenZeppelin's ERC20Permit for gasless approvals in token contracts.
Implement proper slippage protection for DEX-like functionalities.
Use OpenZeppelin's ERC20Votes for governa...
Files:
packages/contracts/contracts/AgentsRegistryUpgradeable.solpackages/contracts/contracts/ServiceRegistryUpgradeable.sol
🧠 Learnings (2)
📚 Learning: 2025-08-01T07:27:26.601Z
Learnt from: CR
PR: ensemble-codes/ensemble-framework#0
File: packages/contracts/.cursor/rules/project-intelligence.mdc:0-0
Timestamp: 2025-08-01T07:27:26.601Z
Learning: Applies to packages/contracts/test/EnsembleCredits*.sol : EnsembleCredits contract must have a comprehensive test suite covering edge cases, security scenarios, gas optimization, and integration with the registry system before deployment
Applied to files:
packages/sdk/test/ensemble.test.ts
📚 Learning: 2025-08-01T07:27:26.601Z
Learnt from: CR
PR: ensemble-codes/ensemble-framework#0
File: packages/contracts/.cursor/rules/project-intelligence.mdc:0-0
Timestamp: 2025-08-01T07:27:26.601Z
Learning: Applies to packages/contracts/test/**/*{Integration,EnsembleCredits,Registry}*.sol : Integration testing must verify interaction between EnsembleCredits and registry contracts
Applied to files:
packages/sdk/test/ensemble.test.ts
🧬 Code graph analysis (3)
packages/sdk/src/services/ServiceRegistryService.ts (4)
packages/sdk/src/index.ts (6)
ServiceRegistryService(50-50)validateRegisterServiceParams(28-28)parseRegisterServiceParams(34-34)validateServiceRecord(26-26)validateUpdateServiceParams(29-29)parseUpdateServiceParams(35-35)packages/sdk/src/schemas/service.schemas.ts (9)
RegisterServiceParams(207-207)ServiceRecord(206-206)validateRegisterServiceParams(237-239)parseRegisterServiceParams(292-306)ServiceStatus(200-200)validateServiceRecord(222-224)UpdateServiceParams(208-208)validateUpdateServiceParams(246-248)parseUpdateServiceParams(313-327)packages/sdk/src/types.ts (4)
RegisterServiceParams(91-91)ServiceRecord(87-87)ServiceStatus(95-95)UpdateServiceParams(92-92)packages/sdk/src/errors.ts (6)
ServiceValidationError(58-63)ServiceAlreadyRegisteredError(22-27)ServiceStatusError(51-56)ServiceNotFoundError(37-42)ServiceAgentAssignmentError(65-70)ServiceOwnershipError(44-49)
packages/sdk/src/schemas/service.schemas.ts (3)
packages/sdk/src/schemas/base.schemas.ts (6)
BigNumberishSchema(21-25)ethereumAddressRegex(11-11)EthereumAddressSchema(49-52)URLSchema(74-76)JsonSchema(31-40)OptionalDateTimeSchema(93-93)packages/sdk/src/types.ts (12)
ServiceCategory(93-93)ServiceMethod(94-94)ServiceStatus(95-95)ServiceOperationalStatus(96-96)ServicePricingModel(97-97)ServicePricing(98-98)ServiceOnChain(89-89)ServiceMetadata(90-90)ServiceRecord(87-87)RegisterServiceParams(91-91)UpdateServiceParams(92-92)Service(88-88)packages/sdk/src/index.ts (17)
validateServiceRecord(26-26)validateService(27-27)validateRegisterServiceParams(28-28)validateUpdateServiceParams(29-29)validateServiceOnChain(30-30)validateServiceMetadata(31-31)parseServiceRecord(32-32)parseService(33-33)parseRegisterServiceParams(34-34)parseUpdateServiceParams(35-35)isServiceRecord(36-36)isService(37-37)isRegisterServiceParams(38-38)isUpdateServiceParams(39-39)isServiceOnChain(40-40)isServiceMetadata(41-41)formatServiceValidationError(42-42)
packages/sdk/src/ensemble.ts (5)
packages/sdk/src/services/ServiceRegistryService.ts (1)
ServiceRegistryService(31-715)packages/sdk/src/index.ts (1)
ServiceRegistryService(50-50)packages/sdk/index.ts (1)
ServiceRegistryService(16-16)packages/sdk/src/schemas/service.schemas.ts (4)
RegisterServiceParams(207-207)ServiceRecord(206-206)UpdateServiceParams(208-208)ServiceStatus(200-200)packages/sdk/src/types.ts (4)
RegisterServiceParams(91-91)ServiceRecord(87-87)UpdateServiceParams(92-92)ServiceStatus(95-95)
🪛 LanguageTool
.taskmaster/docs/prd.txt
[grammar] ~183-~183: There might be a mistake here.
Context: ...chema architecture and validation system - Three-Schema Architecture: Clear separ...
(QB_NEW_EN)
[grammar] ~184-~184: There might be a mistake here.
Context: ...tion of concerns for optimal performance - ServiceOnChainSchema: Minimal blockchain...
(QB_NEW_EN)
[grammar] ~185-~185: There might be a mistake here.
Context: ...hema: Minimal blockchain data (8 fields) - ServiceMetadataSchema: Rich IPFS metadat...
(QB_NEW_EN)
[grammar] ~186-~186: There might be a mistake here.
Context: ...(9+ fields including operational status) - ServiceRecordSchema: Complete combined v...
(QB_NEW_EN)
[grammar] ~189-~189: There might be a mistake here.
Context: ...-state model aligned with business needs - draft: Service being configured, not rea...
(QB_NEW_EN)
[grammar] ~190-~190: There might be a mistake here.
Context: ...vice being configured, not ready for use - published: Service available for discove...
(QB_NEW_EN)
[grammar] ~191-~191: There might be a mistake here.
Context: ...ce available for discovery and execution - archived: Service deprecated but preserv...
(QB_NEW_EN)
[grammar] ~192-~192: There might be a mistake here.
Context: ...ice deprecated but preserved for history - deleted: Service removed (soft delete fo...
(QB_NEW_EN)
[grammar] ~195-~195: There might be a mistake here.
Context: ...validation with enhanced error reporting - validateServiceRecord(): Complete servic...
(QB_NEW_EN)
[grammar] ~196-~196: There might be a mistake here.
Context: ...iceRecord(): Complete service validation - validateServiceOnChain(): On-chain data ...
(QB_NEW_EN)
[grammar] ~197-~197: There might be a mistake here.
Context: ...rviceOnChain(): On-chain data validation - validateServiceMetadata(): IPFS metadata...
(QB_NEW_EN)
[grammar] ~198-~198: There might be a mistake here.
Context: ...viceMetadata(): IPFS metadata validation - validateRegisterServiceParams(): Registr...
(QB_NEW_EN)
[grammar] ~199-~199: There might be a mistake here.
Context: ...ams(): Registration parameter validation - validateUpdateServiceParams(): Update pa...
(QB_NEW_EN)
[grammar] ~202-~202: There might be a mistake here.
Context: ...er Experience**: Full TypeScript support - ServiceRecord: Primary type for SDK cons...
(QB_NEW_EN)
[grammar] ~203-~203: There might be a mistake here.
Context: ...ceRecord: Primary type for SDK consumers - ServiceOnChain: Blockchain data type ...
(QB_NEW_EN)
[grammar] ~204-~204: There might be a mistake here.
Context: ... - ServiceOnChain: Blockchain data type - ServiceMetadata: IPFS metadata type ...
(QB_NEW_EN)
[grammar] ~205-~205: There might be a mistake here.
Context: ... - ServiceMetadata: IPFS metadata type - RegisterServiceParams: Registration inpu...
(QB_NEW_EN)
[grammar] ~206-~206: There might be a mistake here.
Context: ...erServiceParams: Registration input type - UpdateServiceParams: Update input type w...
(QB_NEW_EN)
[grammar] ~209-~209: There might be a mistake here.
Context: ...**: Legacy aliases and migration support - Service type alias → ServiceRecord -...
(QB_NEW_EN)
[grammar] ~210-~210: There might be a mistake here.
Context: ... - Service type alias → ServiceRecord - Deprecated function aliases with clear m...
(QB_NEW_EN)
[grammar] ~211-~211: There might be a mistake here.
Context: ...nction aliases with clear migration path - Gradual migration strategy for existing ...
(QB_NEW_EN)
🔇 Additional comments (16)
packages/contracts/contracts/AgentsRegistryUpgradeable.sol (1)
152-154: Removed combined agent-service registration methodThe V2 architecture separates agent and service management as intended. This removal aligns with the Service Registry V2's independent lifecycle management model.
.taskmaster/tasks/tasks.json (1)
334-402: V2 task comprehensively defined with clear subtask structureThe Service Management System V2 task is well-structured with clear dependencies and subtasks. The architecture aligns perfectly with the Service Registry V2 implementation.
packages/sdk/test/ensemble.test.ts (3)
196-227: Service registration test correctly uses V2 metadata structureThe test properly validates the new V2 service registration with nested metadata and returns the complete ServiceRecord.
266-291: New V2 service methods properly testedGood coverage of the new getServiceById method with proper assertions on the returned ServiceRecord structure.
337-387: Comprehensive listServices test with proper filteringExcellent test coverage for the new listServices method including various filter combinations and proper ServiceRecord array validation.
packages/sdk/src/ensemble.ts (3)
47-49: Service registry getter properly exposes V2 functionalityThe new
servicesgetter provides clean access to the ServiceRegistryService instance, enabling direct use of V2-specific methods.
282-285: Service registration correctly uses RegisterServiceParamsThe method signature and implementation properly align with the V2 service registration pattern using RegisterServiceParams.
301-348: V2 service methods comprehensive and well-documentedThe new service methods (getServiceById, updateService, deleteService, listServices) provide a complete V2 API surface with proper JSDoc documentation and type safety.
.taskmaster/docs/prd.txt (3)
31-60: Service Registry V2 requirements comprehensively documentedThe PRD properly documents the hybrid on-chain/off-chain architecture with clear separation of concerns between blockchain and IPFS storage. The service-agent assignment model (REQ-1.3) and advanced discovery features (REQ-1.4) align well with the implementation.
153-182: ServiceRegistryService API requirements match implementationREQ-4.1.2 accurately describes the implemented service registry APIs including registerService, getService, updateService, getAllServices, and activateService with proper IPFS integration.
183-213: Three-schema architecture properly specifiedREQ-4.1.3 clearly defines the three-schema architecture (ServiceOnChainSchema, ServiceMetadataSchema, ServiceRecordSchema) and the 4-state lifecycle model, which aligns with the actual implementation.
packages/sdk/src/abi/ServiceRegistry.json (1)
1-730: LGTM! Comprehensive V2 contract interface.The ABI properly reflects the V2 service registry architecture with:
- ID-based service management
- Comprehensive lifecycle operations
- Agent assignment functionality
- Proper event definitions for state changes
- UUPS upgradeability support
packages/contracts/contracts/ServiceRegistryUpgradeable.sol (2)
17-21: ServiceStatus enum values well-defined.The lifecycle states (DRAFT, PUBLISHED, ARCHIVED, DELETED) provide clear service state management with appropriate transitions enforced.
87-116: registerService implementation is robust.The function properly:
- Auto-increments service IDs
- Initializes service with DRAFT status
- Updates all necessary mappings
- Emits appropriate events
packages/sdk/src/schemas/service.schemas.ts (2)
136-147: Published service validation is good!The refinement correctly enforces that published services must have a non-zero agent address, maintaining data integrity.
292-306: Enhanced error messages in parseRegisterServiceParams.Good practice to prefix error messages with context ("Service registration:") for better debugging experience.
| { | ||
| "id": 1, | ||
| "title": "Extend ServiceRegistryService with V2 CRUD Operations and Ownership Model", | ||
| "description": "Enhance the existing ServiceRegistryService class with comprehensive CRUD operations, ownership model, and service lifecycle management capabilities.", | ||
| "dependencies": [], | ||
| "details": "Extend the ServiceRegistryService class to include: createService(), updateService(), deleteService(), getService(), listServices() methods with proper error handling and validation. Implement ownership model with transferOwnership(), getServiceOwner(), and ownership validation methods. Add service lifecycle management with status tracking (draft, active, paused, archived, deleted). Include proper TypeScript interfaces and error handling for all operations. Ensure backward compatibility with existing service registry functionality.", | ||
| "status": "done", | ||
| "testStrategy": "Unit tests for all CRUD operations with mocked blockchain interactions. Test ownership model operations including transfers and permission checks. Validate service lifecycle state transitions and status management." |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
ServiceRegistry CRUD operations marked as done
Subtask 25.1 status shows "done" for the V2 CRUD operations and ownership model. Based on the code review, these are indeed implemented in ServiceRegistryService.ts.
🏁 Script executed:
#!/bin/bash
# Verify all CRUD methods are implemented in ServiceRegistryService
ast-grep --pattern 'async $METHOD($$$): Promise<$_> {
$$$
}' packages/sdk/src/services/ServiceRegistryService.ts | grep -E "(createService|updateService|deleteService|getService|listServices|transferOwnership|getServiceOwner)"Length of output: 230
🏁 Script executed:
#!/bin/bash
# Locate the ServiceRegistryService class file
echo "Finding ServiceRegistryService definition..."
rg -nP 'class\s+ServiceRegistryService\b' -g '*.ts'
# Search for CRUD and ownership method definitions across TS files
echo "Checking for method implementations..."
rg -nP '^(?:\s*(?:public|private|protected)\s+)?(?:async\s+)?(?:static\s+)?(createService|updateService|deleteService|getService|listServices|transferOwnership|getServiceOwner)\s*\(' -n -g '*.ts'Length of output: 530
🏁 Script executed:
#!/bin/bash
# Search for CRUD and ownership method names within ServiceRegistryService.ts
file="packages/sdk/src/services/ServiceRegistryService.ts"
echo "Searching in $file..."
rg -n 'createService' "$file"
rg -n 'updateService' "$file"
rg -n 'deleteService' "$file"
rg -n 'getService\(' "$file"
rg -n 'listServices' "$file"
rg -n 'transferOwnership' "$file"
rg -n 'getServiceOwner' "$file"Length of output: 1759
Implement missing ServiceRegistryService V2 methods
- ServiceRegistryService.ts (packages/sdk/src/services/ServiceRegistryService.ts) is missing createService(...) and transferOwnership(...) implementations.
- listServices(...) currently only logs “not yet fully implemented” – replace the placeholder warning with full V2 logic.
🤖 Prompt for AI Agents
In .taskmaster/tasks/tasks.json around lines 349 to 356, the task entry
documents that packages/sdk/src/services/ServiceRegistryService.ts is missing
implementations for createService(...) and transferOwnership(...), and
listServices(...) contains only a placeholder log; implement the full V2 logic
by adding createService to validate inputs, construct and persist the new
service entity (including status, owner, metadata, timestamps), integrate with
existing registry/storage and any blockchain call shims with proper error
handling and unit-test hooks; implement transferOwnership to verify permissions,
validate new owner, update ownership atomically, emit events/audit logs and
handle blockchain interactions and rollback on failure; replace the listServices
placeholder with a real V2 listing that supports filters, pagination, status
filtering, and maps storage/chain records to the V2 DTOs; ensure all methods
include TypeScript interfaces, comprehensive error handling,
permission/ownership checks, and unit tests/mocks to match the task's
testStrategy.
.taskmaster/tasks/tasks.json
Outdated
| "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." |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Smart contract V2 functionality still pending
Subtask 25.3 for updating smart contracts with V2 functionality is marked as "pending". This is critical for the full V2 implementation to work correctly.
The ServiceRegistry smart contract needs to be updated to support:
- ID-based service management
- Ownership transfers
- Agent assignments
- Hybrid storage pattern with IPFS
This is blocking full V2 functionality. The SDK implementation (ServiceRegistryService.ts) has placeholder error messages indicating the smart contract support is missing.
Would you like me to help implement the smart contract updates or create a detailed specification for the required contract methods?
🤖 Prompt for AI Agents
In .taskmaster/tasks/tasks.json around lines 377-378, Subtask 25.3 is still
marked "pending" because the ServiceRegistry smart contract lacks V2 support
(ID-based service management, ownership transfers, agent assignments, and hybrid
IPFS storage), and the SDK has placeholder errors; update the project by (1)
adding new Solidity functions/events to ServiceRegistry: createService(id,
dataHash), updateService(id, dataHash), transferOwnership(id, newOwner),
assignAgent(id, agent, permissions), revokeAgent(id, agent), and events for
ServiceCreated/Updated/OwnershipTransferred/AgentAssigned; (2) implement hybrid
on-chain/off-chain storage by storing IPFS content hashes on-chain and full
payloads in IPFS, with access control checks in modifiers; (3) write
comprehensive unit tests for these new functions and gas/permission checks; and
(4) remove placeholder errors and implement corresponding methods in
ServiceRegistryService.ts to call the new contract functions and handle
errors/exceptions with proper logging.
| // V2: Service validation removed - services use IDs and are managed independently | ||
| // require( | ||
| // serviceRegistry.isServiceRegistered(serviceName), | ||
| // "Service not registered" | ||
| // ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Service validation disabled but not replaced with V2 validation
Service validation has been commented out but there's no replacement with the new V2 service ID-based validation. This allows proposals to be added for non-existent services, potentially creating inconsistencies.
Apply this diff to properly validate services using V2:
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"
- // );
+ // V2: Service validation using service ID or name lookup
+ // TODO: Replace with service ID-based validation when V2 contract is updated
+ // For now, skip validation as services are managed independently
_createProposal(agent, serviceName, servicePrice, tokenAddress);
}Would you like me to implement proper V2 service validation once the ServiceRegistry contract supports service ID lookups?
Committable suggestion skipped: line range outside the PR's diff.
| // 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 | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Migration function disabled without V2 alternative
The _ensureServiceRegistered function has been disabled but the migration path for V1 services to V2 is unclear. The comment indicates V2 uses IDs and different parameters, but there's no implementation.
Consider implementing a V2-compatible migration strategy:
-// 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
+// V2: Migration helper - creates a basic V2 service if not exists
+function _ensureServiceRegisteredV2(string memory serviceName) private {
+ // Check if we need to migrate this service to V2
+ // This would require ServiceRegistry V2 to expose a method to check by name
+ // For now, log the service name for manual migration
+ emit ServiceMigrationRequired(serviceName);
}🏁 Script executed:
#!/bin/bash
# Check if ServiceRegistry V2 has methods for service migration or name-based lookup
ast-grep --pattern 'function $_(string memory serviceName) $$$' packages/contracts/contracts/ServiceRegistryUpgradeable.sol
rg -n "serviceName|getServiceByName|migrateService" packages/contracts/contracts/ServiceRegistryUpgradeable.solLength of output: 325
Replace no-op migration stub with event and off-chain process
_ensureServiceRegistered (AgentsRegistryUpgradeable.sol:365–370) is empty because ServiceRegistry V2 removed on-chain name lookup and stores names off-chain in IPFS metadata (ServiceRegistryUpgradeable.sol:127). On-chain migration by name is unsupported—emit a ServiceMigrationRequired(bytes32 serviceId, address agentAddress) event during upgrade and perform actual registerService(id, serviceUri, agentAddress) calls off-chain by retrieving each service’s metadata from IPFS.
🤖 Prompt for AI Agents
In packages/contracts/contracts/AgentsRegistryUpgradeable.sol around lines
365–370, the _ensureServiceRegistered migration stub is a no-op but must signal
off-chain migration; add a new event declaration
ServiceMigrationRequired(bytes32 indexed serviceId, address indexed
agentAddress) to the contract and modify _ensureServiceRegistered to emit that
event for each agent/service pair that requires migration (do not perform
on-chain name lookups or register calls); document that operators must run an
off-chain migration process which fetches service metadata from IPFS using the
stored serviceId, extracts serviceUri and name, then calls the new
ServiceRegistry.registerService(serviceId, serviceUri, agentAddress) off-chain
via transactions. Ensure the function remains non-reverting and only emits the
event so on-chain upgrade completes safely.
| // V2: Service migration disabled | ||
| // _ensureServiceRegistered(proposal.serviceName); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Service migration call commented out during agent migration
The service migration is disabled during agent migration from V1, which could leave agents with proposals for services that don't exist in V2.
Consider handling this scenario explicitly:
// V2: Service migration disabled
-// _ensureServiceRegistered(proposal.serviceName);
+// Skip service migration - services are managed independently in V2
+// Note: This proposal references a V1 service that may not exist in V2
+emit V1ProposalMigrated(agent, proposal.serviceName, i);Would you like me to create a comprehensive migration strategy that handles V1 proposals with proper V2 service mapping?
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In packages/contracts/contracts/AgentsRegistryUpgradeable.sol around lines
385-386, the call to _ensureServiceRegistered was commented out during V2
migration leaving V1 proposals potentially referencing non-existent V2 services;
fix by adding an explicit migration step that iterates V1 proposals and for each
proposal checks whether the referenced service exists in V2, if it does nothing,
if it does not then (a) try to map the old service name to a V2 name via a
migration mapping (add a mapping param or storage field for migration name
pairs), and if a mapping exists update the proposal to the mapped name and call
_ensureServiceRegistered for that mapped name, (b) if no mapping exists either
register a new service entry preserving needed metadata via a migrateService
helper or mark the proposal invalid/expired and emit an event to surface the
action; include proper access control for migration, add events for
mapping/registration/rejection, and add unit tests covering mapped, newly
registered, and rejected proposals.
| async getService(name: string): Promise<ServiceRecord> { | ||
| return this.serviceRegistryService.getService(name); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Legacy getService method retained but may cause confusion
The getService(name: string) method is retained alongside the new getServiceById(serviceId: string). Since V2 services use IDs rather than names, this could cause confusion.
Consider deprecating the name-based lookup or clarifying its limited use:
/**
* Gets a service by name.
+ * @deprecated Use getServiceById() instead. V2 services primarily use IDs.
* @param {string} name - The name of the service.
* @returns {Promise<ServiceRecord>} A promise that resolves to the service.
+ * @throws {Error} Service names are stored off-chain in V2 and may not support direct lookup.
*/
async getService(name: string): Promise<ServiceRecord> {
return this.serviceRegistryService.getService(name);
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| async getService(name: string): Promise<ServiceRecord> { | |
| return this.serviceRegistryService.getService(name); | |
| } | |
| /** | |
| * Gets a service by name. | |
| * @deprecated Use getServiceById() instead. V2 services primarily use IDs. | |
| * @param {string} name - The name of the service. | |
| * @returns {Promise<ServiceRecord>} A promise that resolves to the service. | |
| * @throws {Error} Service names are stored off-chain in V2 and may not support direct lookup. | |
| */ | |
| async getService(name: string): Promise<ServiceRecord> { | |
| return this.serviceRegistryService.getService(name); | |
| } |
🤖 Prompt for AI Agents
In packages/sdk/src/ensemble.ts around lines 292 to 294, the retained
getService(name: string) method can confuse consumers because V2 services use
service IDs; mark this method as deprecated and guide users to the new
getServiceById(serviceId: string). Specifically, add a deprecation JSDoc comment
on getService mentioning getServiceById, log or emit a one-time warning when
called (or throw if you want to enforce migration), and update its
implementation to delegate to getServiceById when possible (e.g., resolve name
to id) or return a clear error indicating name-based lookups are legacy.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (6)
packages/contracts/scripts/deploy.ts (1)
50-58: Avoid hard exiting; use exitCode.Process exits in success paths can interfere with Hardhat runners and CI aggregation.
-main() - .then(() => { - console.log("Deployment completed successfully!"); - process.exit(0); - }) - .catch((error) => { - console.error("Error during deployment:", error); - process.exit(1); - }); +main() + .then(() => { + console.log("Deployment completed successfully!"); + }) + .catch((error) => { + console.error("Error during deployment:", error); + process.exitCode = 1; + });packages/contracts/contracts/AgentsRegistryUpgradeable.sol (5)
43-49: Use custom errors and explicit registration check in modifiersReplace string reverts with custom errors and add a separate “onlyRegisteredAgent” guard to standardize errors and save gas.
+ // Custom errors + error NotAgentOwner(address caller, address owner, address agent); + error AgentNotRegistered(address agent); + - modifier onlyAgentOwner(address agent) { - require( - agents[agent].owner == msg.sender, - "Not the owner of the agent" - ); - _; - } + modifier onlyRegisteredAgent(address agent) { + if (agents[agent].agent == address(0)) revert AgentNotRegistered(agent); + _; + } + modifier onlyAgentOwner(address agent) { + if (agents[agent].owner != msg.sender) { + revert NotAgentOwner(msg.sender, agents[agent].owner, agent); + } + _; + }
90-97: Emit event and use custom error for service registry updatesState change should emit an event; replace string revert with custom error.
+ event ServiceRegistryUpdated(address indexed oldRegistry, address indexed newRegistry, address indexed caller); + error InvalidAddress(address addr); @@ - function setServiceRegistry(address _serviceRegistry) external onlyOwner { - require(_serviceRegistry != address(0), "Invalid address"); - serviceRegistry = ServiceRegistryUpgradeable(_serviceRegistry); - } + function setServiceRegistry(address _serviceRegistry) external onlyOwner { + if (_serviceRegistry == address(0)) revert InvalidAddress(_serviceRegistry); + address old = address(serviceRegistry); + serviceRegistry = ServiceRegistryUpgradeable(_serviceRegistry); + emit ServiceRegistryUpdated(old, _serviceRegistry, msg.sender); + }
112-120: Validate non-zero agent address and switch to custom errorPrevent registering zero address; use custom error for duplicates.
+ error AgentAlreadyRegistered(address agent); @@ - ) external { - require(agents[agent].agent == address(0), "Agent already registered"); + ) external { + if (agent == address(0)) revert InvalidAddress(agent); + if (agents[agent].agent != address(0)) revert AgentAlreadyRegistered(agent); _createAgent(agent, name, agentUri, msg.sender, 0); }
145-162: addRating: allow-list agent existence and use custom errorAvoid rating non-existent agents; drop redundant
>= 0check for uints; use custom error.+ error InvalidRating(uint256 rating); @@ - ) public returns (uint256) { - require( - _rating >= 0 && _rating <= 100, - "Rating must be between 0 and 100" - ); - agents[agent].totalRatings += 1; + ) public onlyRegisteredAgent(agent) returns (uint256) { + if (_rating > 100) revert InvalidRating(_rating); + agents[agent].totalRatings += 1; agents[agent].reputation = (agents[agent].reputation * (agents[agent].totalRatings - 1) + _rating) / agents[agent].totalRatings; emit ReputationUpdated(agent, agents[agent].reputation);
198-209: Order modifiers to ensure “not registered” surfaces correctlyApply
onlyRegisteredAgentbeforeonlyAgentOwnerto distinguish errors.- ) external onlyAgentOwner(agent) { - require(agents[agent].agent != address(0), "Agent not registered"); + ) external onlyRegisteredAgent(agent) onlyAgentOwner(agent) { agents[agent].name = name; agents[agent].agentUri = agentUri; emit AgentDataUpdated(agent, name, agentUri); }
🧹 Nitpick comments (11)
packages/contracts/scripts/deploy.ts (1)
45-46: Augment the summary with network for reproducibility.Including the network name helps correlate addresses across chains.
- console.log("\nNote: TaskRegistry has been removed as it's now legacy."); - console.log("Services and agents are managed independently through their respective registries."); + console.log(`\nNetwork: ${hre.network.name}`); + console.log("Note: TaskRegistry has been removed as it's now legacy."); + console.log("Services and agents are managed independently through their respective registries.");packages/contracts/ignition/modules/DeployAll.ts (3)
25-27:initialAdminparam is declared but unused.Either wire it into sub-modules or drop it from the API/docs to avoid confusion.
- const initialAdmin = m.getParameter("initialAdmin", m.getAccount(0)); + // Remove if not propagated to sub-modules: + // const initialAdmin = m.getParameter("initialAdmin", m.getAccount(0));Or forward it if supported:
// Example if modules accept it: // const { ensembleCredits } = m.useModule(EnsembleCreditsModule, { initialAdmin }); // const { serviceRegistry } = m.useModule(ServiceRegistryUpgradeableModule, { initialAdmin });
34-35: Explicitly forwardv1RegistryAddress(single point of override).Surface the param at this top-level module and pass it to AgentsRegistry module to avoid editing multiple files when overriding.
- const { agentsRegistry, agentsRegistryProxy, agentsRegistryImpl } = m.useModule(AgentsRegistryUpgradeableModule); + const v1RegistryAddress = m.getParameter("v1RegistryAddress", "0x0000000000000000000000000000000000000000"); + const { agentsRegistry, agentsRegistryProxy, agentsRegistryImpl } = + m.useModule(AgentsRegistryUpgradeableModule, { v1RegistryAddress });
51-53: Consider exposing ServiceRegistry proxy/impl for symmetry.If
ServiceRegistryUpgradeableModuleprovides these, exporting them here simplifies upgrades and verifications.// Example: // const { serviceRegistry, serviceRegistryProxy, serviceRegistryImpl } = m.useModule(ServiceRegistryUpgradeableModule); // return { ..., serviceRegistry, serviceRegistryProxy, serviceRegistryImpl, agentsRegistry, agentsRegistryProxy, agentsRegistryImpl };packages/contracts/scripts/deploy.js (1)
32-33: Good note; gate legacy messaging by env flagLogging is fine. Consider gating these notes (and the mock V1 deployment) behind an env flag (e.g., LEGACY_COMPAT=true) so prod deployments are clean.
- console.log("\nNote: TaskRegistry has been removed as it's now legacy."); - console.log("Services and agents are managed independently through their respective registries."); + if (process.env.LEGACY_COMPAT) { + console.log("\nNote: TaskRegistry has been removed as it's now legacy."); + console.log("Services and agents are managed independently through their respective registries."); + }packages/contracts/test/AgentRegistry.test.js (1)
64-68: Assert specific revert for double-initializeUse
.to.be.revertedWithCustomErroror explicit message to harden the assertion.- await expect( - agentsRegistry.initialize(await mockV1Registry.getAddress(), await serviceRegistry.getAddress()) - ).to.be.reverted; + await expect( + agentsRegistry.initialize(await mockV1Registry.getAddress(), await serviceRegistry.getAddress()) + ).to.be.reverted; // consider .to.be.revertedWithCustomError(agentsRegistry, "InvalidInitialization")packages/contracts/test/ServiceRegistry.test.js (2)
27-31: Verify meaning of nextServiceId
nextServiceId()starts at 0 and equals 1 after first registration. Name suggests “next to use,” but behavior matches “last issued.” Confirm intent or rename in contract/tests for clarity.
110-114: Prefer custom errors over revert stringsIf the contract uses custom errors, update this assertion to
.to.be.revertedWithCustomError(registry, "EmptyServiceURI").packages/contracts/contracts/AgentsRegistryUpgradeable.sol (3)
126-137: Migration: add event, guard source presenceEmit an event for observability and optionally check that V1 has data.
+ event AgentMigrated(address indexed agent, address indexed owner, string name, string agentUri, uint256 reputation); @@ - IAgentRegistryV1.AgentData memory v1AgentData = IAgentRegistryV1(agentRegistryV1).getAgentData(agent); + IAgentRegistryV1.AgentData memory v1AgentData = IAgentRegistryV1(agentRegistryV1).getAgentData(agent); @@ _createAgent(agent, v1AgentData.name, v1AgentData.agentUri, v1AgentData.owner, v1AgentData.reputation); + emit AgentMigrated(agent, v1AgentData.owner, v1AgentData.name, v1AgentData.agentUri, v1AgentData.reputation);
41-42: Limit surface area: make mapping private
agentsis exposed publicly and you already havegetAgentData. Consider making itprivateto keep API minimal and flexible for upgrades.- mapping(address => AgentData) public agents; + mapping(address => AgentData) private agents;
56-70: NatSpec consistencyNatSpec exists, but add
@noticeto all public/external functions to meet the “complete NatSpec” guideline.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (14)
packages/contracts/contracts/AgentsRegistryUpgradeable.sol(6 hunks)packages/contracts/contracts/TaskRegistryUpgradeable.sol(0 hunks)packages/contracts/contracts/interfaces/IProposalStruct.sol(0 hunks)packages/contracts/ignition/modules/DeployAll.ts(3 hunks)packages/contracts/ignition/modules/TaskRegistryUpgradeable.ts(0 hunks)packages/contracts/ignition/modules/UpgradeRegistries.ts(1 hunks)packages/contracts/ignition/params/baseSepolia.json(1 hunks)packages/contracts/ignition/params/local.json(1 hunks)packages/contracts/scripts/create-task.ts(0 hunks)packages/contracts/scripts/deploy.js(1 hunks)packages/contracts/scripts/deploy.ts(2 hunks)packages/contracts/test/AgentRegistry.test.js(1 hunks)packages/contracts/test/ServiceRegistry.test.js(1 hunks)packages/contracts/test/TaskRegistry.test.js(0 hunks)
💤 Files with no reviewable changes (5)
- packages/contracts/contracts/interfaces/IProposalStruct.sol
- packages/contracts/scripts/create-task.ts
- packages/contracts/ignition/modules/TaskRegistryUpgradeable.ts
- packages/contracts/contracts/TaskRegistryUpgradeable.sol
- packages/contracts/test/TaskRegistry.test.js
🧰 Additional context used
📓 Path-based instructions (2)
packages/contracts/contracts/**/*.sol
📄 CodeRabbit inference engine (packages/contracts/.cursor/rules/project-intelligence.mdc)
packages/contracts/contracts/**/*.sol: All public functions in Solidity contracts must have complete NatSpec documentation comments
Custom errors with descriptive names should be used for error handling in Solidity contracts instead of string messages
All state changes in Solidity contracts must emit appropriate events
Follow OpenZeppelin security patterns in all Solidity contracts
Files:
packages/contracts/contracts/AgentsRegistryUpgradeable.sol
packages/contracts/**/*.sol
📄 CodeRabbit inference engine (packages/contracts/.cursor/rules/role.mdc)
packages/contracts/**/*.sol: Use explicit function visibility modifiers and appropriate natspec comments.
Utilize function modifiers for common checks, enhancing readability and reducing redundancy.
Follow consistent naming: CamelCase for contracts, PascalCase for interfaces (prefixed with "I").
Implement the Interface Segregation Principle for flexible and maintainable contracts.
Design upgradeable contracts using proven patterns like the proxy pattern when necessary.
Implement comprehensive events for all significant state changes.
Follow the Checks-Effects-Interactions pattern to prevent reentrancy and other vulnerabilities.
Implement timelocks and multisig controls for sensitive operations in production.
Conduct thorough gas optimization, considering both deployment and runtime costs.
Use OpenZeppelin's AccessControl for fine-grained permissions.
Use Solidity 0.8.0+ for built-in overflow/underflow protection.
Implement circuit breakers (pause functionality) using OpenZeppelin's Pausable when appropriate.
Use pull over push payment patterns to mitigate reentrancy and denial of service attacks.
Implement rate limiting for sensitive functions to prevent abuse.
Use OpenZeppelin's SafeERC20 for interacting with ERC20 tokens.
Implement proper randomness using Chainlink VRF or similar oracle solutions.
Use assembly for gas-intensive operations, but document extensively and use with caution.
Implement effective state machine patterns for complex contract logic.
Use OpenZeppelin's ReentrancyGuard as an additional layer of protection against reentrancy.
Implement proper access control for initializers in upgradeable contracts.
Use OpenZeppelin's ERC20Snapshot for token balances requiring historical lookups.
Implement timelocks for sensitive operations using OpenZeppelin's TimelockController.
Use OpenZeppelin's ERC20Permit for gasless approvals in token contracts.
Implement proper slippage protection for DEX-like functionalities.
Use OpenZeppelin's ERC20Votes for governa...
Files:
packages/contracts/contracts/AgentsRegistryUpgradeable.sol
🧠 Learnings (5)
📚 Learning: 2025-08-01T07:27:26.601Z
Learnt from: CR
PR: ensemble-codes/ensemble-framework#0
File: packages/contracts/.cursor/rules/project-intelligence.mdc:0-0
Timestamp: 2025-08-01T07:27:26.601Z
Learning: Applies to packages/contracts/test/*Registry*.sol : All registry contracts must have extensive test coverage, including edge cases and integration scenarios
Applied to files:
packages/contracts/test/ServiceRegistry.test.jspackages/contracts/test/AgentRegistry.test.js
📚 Learning: 2025-08-01T07:27:26.601Z
Learnt from: CR
PR: ensemble-codes/ensemble-framework#0
File: packages/contracts/.cursor/rules/project-intelligence.mdc:0-0
Timestamp: 2025-08-01T07:27:26.601Z
Learning: Applies to packages/contracts/test/**/*{Integration,EnsembleCredits,Registry}*.sol : Integration testing must verify interaction between EnsembleCredits and registry contracts
Applied to files:
packages/contracts/test/ServiceRegistry.test.jspackages/contracts/test/AgentRegistry.test.js
📚 Learning: 2025-08-01T07:27:26.601Z
Learnt from: CR
PR: ensemble-codes/ensemble-framework#0
File: packages/contracts/.cursor/rules/project-intelligence.mdc:0-0
Timestamp: 2025-08-01T07:27:26.601Z
Learning: Applies to packages/contracts/test/EnsembleCredits*.sol : EnsembleCredits contract must have a comprehensive test suite covering edge cases, security scenarios, gas optimization, and integration with the registry system before deployment
Applied to files:
packages/contracts/test/ServiceRegistry.test.jspackages/contracts/test/AgentRegistry.test.js
📚 Learning: 2025-08-01T07:27:59.616Z
Learnt from: CR
PR: ensemble-codes/ensemble-framework#0
File: packages/contracts/.cursor/rules/role.mdc:0-0
Timestamp: 2025-08-01T07:27:59.616Z
Learning: Applies to packages/contracts/**/*.sol : Design upgradeable contracts using proven patterns like the proxy pattern when necessary.
Applied to files:
packages/contracts/ignition/modules/UpgradeRegistries.tspackages/contracts/contracts/AgentsRegistryUpgradeable.sol
📚 Learning: 2025-08-01T07:27:59.616Z
Learnt from: CR
PR: ensemble-codes/ensemble-framework#0
File: packages/contracts/.cursor/rules/role.mdc:0-0
Timestamp: 2025-08-01T07:27:59.616Z
Learning: Applies to packages/contracts/**/*.sol : Implement proper access control for initializers in upgradeable contracts.
Applied to files:
packages/contracts/contracts/AgentsRegistryUpgradeable.sol
🔇 Additional comments (6)
packages/contracts/ignition/params/baseSepolia.json (1)
5-5: No remaining TaskRegistry params/consumers
Verified thatpackages/contracts/ignition/modulescontains no imports orm.getParametercalls forTaskRegistryUpgradeableModule,initialTaskId,taskRegistryProxy, ornewTaskRegistryImpl.packages/contracts/ignition/params/local.json (1)
5-5: Mirror cleanup locally; verify repo-wide references.Same as baseSepolia: ensure no remaining local references to TaskRegistry params.
Use the same script from the previous comment at the repo root.
packages/contracts/ignition/modules/UpgradeRegistries.ts (2)
50-55: No downstream references to removed TaskRegistry exports found. Ripgrep searches fornewTaskRegistryImplandtaskRegistryProxyacross the codebase returned no matches—safe to update the return shape as-is.
41-47: Be explicit about upgrade caller, initialization data, and run storage layout validation
- Add a
fromoption to eachm.callso the upgrade is sent from the account authorized by your_authorizeUpgradeoverride (e.g.from: m.getAccount(0)).- If the new implementation has an initializer or migration step, encode and pass its initializer payload instead of the empty
"0x"bytes.- Before executing upgrades, verify both the old and new implementations support
upgradeToAndCalland compare their storage layouts (e.g. using OpenZeppelin’s layout validation against your actual deployment artifacts).packages/contracts/ignition/modules/DeployAll.ts (1)
7-16: Docs update LGTM.Clear V2 orientation and legacy TaskRegistry note read well.
packages/contracts/contracts/AgentsRegistryUpgradeable.sol (1)
254-262: Upgrade auth OKOwner-gated UUPS upgrade path looks correct.
| 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); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
removeAgent: rely on modifiers, drop duplicate require
Unify error reporting and save gas; also apply registered check first.
- function removeAgent(address agent) external onlyAgentOwner(agent) {
- require(agents[agent].agent != address(0), "Agent not registered");
+ function removeAgent(address agent) external onlyRegisteredAgent(agent) onlyAgentOwner(agent) {
address agentOwner = agents[agent].owner;
delete agents[agent];
emit AgentRemoved(agent, agentOwner);
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| 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); | |
| } | |
| function removeAgent(address agent) external onlyRegisteredAgent(agent) onlyAgentOwner(agent) { | |
| address agentOwner = agents[agent].owner; | |
| delete agents[agent]; | |
| emit AgentRemoved(agent, agentOwner); | |
| } |
🤖 Prompt for AI Agents
In packages/contracts/contracts/AgentsRegistryUpgradeable.sol around lines 222
to 231, remove the inline require("Agent not registered") and instead enforce
the registration check via modifiers to unify error messages and save gas: add
(or reuse) a onlyRegistered(address agent) modifier that requires
agents[agent].agent != address(0) with the "Agent not registered" revert, then
change the function signature to use onlyRegistered(agent) before
onlyAgentOwner(agent) (i.e. function removeAgent(address agent) external
onlyRegistered(agent) onlyAgentOwner(agent)), keep retrieving agentOwner after
the registration check, delete agents[agent], and emit AgentRemoved(agent,
agentOwner).
| // Deploy mock V1 registry for migration compatibility | ||
| mockV1Registry = await upgrades.deployProxy(ServiceRegistry, [], { | ||
| initializer: "initialize", | ||
| kind: "uups" | ||
| }); | ||
| await mockV1Registry.waitForDeployment(); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mock type mismatch for migrate path
mockV1Registry is a ServiceRegistry proxy, not an IAgentRegistryV1. Any migrateAgent call will revert. Replace with a minimal mock that implements IAgentRegistryV1.getAgentData.
- mockV1Registry = await upgrades.deployProxy(ServiceRegistry, [], {
- initializer: "initialize",
- kind: "uups"
- });
+ const MockV1 = await ethers.getContractFactory("MockAgentRegistryV1");
+ mockV1Registry = await MockV1.deploy();
+ await mockV1Registry.waitForDeployment();Add mock (new file suggestion):
// packages/contracts/contracts/mocks/MockAgentRegistryV1.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;
import "../interfaces/IAgentRegistryV1.sol";
contract MockAgentRegistryV1 is IAgentRegistryV1 {
mapping(address => AgentData) public data;
function setAgentData(address a, AgentData calldata d) external { data[a] = d; }
function getAgentData(address a) external view returns (AgentData memory) { return data[a]; }
}🤖 Prompt for AI Agents
In packages/contracts/test/AgentRegistry.test.js around lines 30-36, the test
currently deploys a ServiceRegistry proxy as mockV1Registry but migrateAgent
expects an IAgentRegistryV1 implementation; replace this by adding a new
MockAgentRegistryV1.sol implementing IAgentRegistryV1.getAgentData (as
suggested) in packages/contracts/contracts/mocks, then in the test deploy that
mock via ethers.getContractFactory(...).deploy() (not upgrades.deployProxy),
cast/use its address when calling migrateAgent, and in the test setAgentData on
the mock for the agent(s) you expect to migrate before invoking migrateAgent so
the call won’t revert.
| it("Should fail to update non-existent agent", async function () { | ||
| await expect( | ||
| registry.connect(agentOwner).registerAgentWithService( | ||
| agentAddress, | ||
| "Service Agent", | ||
| agentUri, | ||
| "Service1", | ||
| ethers.parseEther("0.01"), | ||
| ethers.ZeroAddress | ||
| agentsRegistry.connect(addr1).setAgentData( | ||
| agentAddr2.address, | ||
| "New Name", | ||
| "New URI" | ||
| ) | ||
| ).to.be.revertedWith("Agent already registered"); | ||
| ).to.be.revertedWith("Not the owner of the agent"); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Ambiguous revert on non-existent agent
Currently reverts with "Not the owner of the agent" when agent is unregistered. Prefer a distinct "Agent not registered" error to avoid confusion; update test accordingly if contract is changed.
- ).to.be.revertedWith("Not the owner of the agent");
+ ).to.be.revertedWith("Agent not registered")🏁 Script executed:
#!/bin/bash
# List all Solidity files to locate AgentRegistry.sol
echo "Solidity files in repo:"
fd -e sol
# Search for the setAgentData function implementation
echo ""
echo "setAgentData implementation and surrounding code:"
rg -n 'function setAgentData' -C10 .Length of output: 67358
Swap registration and ownership checks in setAgentData
In packages/contracts/contracts/AgentsRegistryUpgradeable.sol#setAgentData, the onlyAgentOwner modifier fires first—causing unregistered agents to revert with “Not the owner of the agent.” Move the require(agents[agent].agent != address(0), "Agent not registered") check before the ownership check (or include it in the modifier), then update packages/contracts/test/AgentRegistry.test.js to expect “Agent not registered” when calling setAgentData on a non‐existent agent.
🤖 Prompt for AI Agents
In packages/contracts/test/AgentRegistry.test.js around lines 167 to 175, the
test expects "Not the owner of the agent" when calling setAgentData on an
unregistered agent, but the contract validation order should first check
registration; update the contract so require(agents[agent].agent != address(0),
"Agent not registered") runs before the ownership check (or include that check
in the onlyAgentOwner modifier), then change this test to expect
revertWith("Agent not registered") for the non-existent agent case.
| it("Should set service registry by owner", async function () { | ||
| const newServiceRegistry = await upgrades.deployProxy(ServiceRegistry, [], { | ||
| initializer: "initialize", | ||
| kind: "uups" | ||
| }); | ||
| await newServiceRegistry.waitForDeployment(); | ||
|
|
||
| it("Should allow updating only URI", async function () { | ||
| const originalAgentData = await registry.getAgentData(agentAddress); | ||
|
|
||
| await registry.connect(agentOwner).setAgentData( | ||
| agentAddress, | ||
| originalAgentData.name, // Keep original name | ||
| newAgentUri | ||
| await agentsRegistry.connect(owner).setServiceRegistry( | ||
| await newServiceRegistry.getAddress() | ||
| ); | ||
|
|
||
| const updatedAgentData = await registry.getAgentData(agentAddress); | ||
| expect(updatedAgentData.name).to.equal(originalAgentData.name); | ||
| expect(updatedAgentData.agentUri).to.equal(newAgentUri); | ||
| }); | ||
|
|
||
| it("Should preserve reputation and ratings after update", async function () { | ||
| // Add some reputation first | ||
| await registry.connect(admin).setTaskRegistry(admin); | ||
| await registry.connect(admin).addRating(agentAddress, 80); | ||
|
|
||
| const originalAgentData = await registry.getAgentData(agentAddress); | ||
| expect(originalAgentData.reputation).to.equal(80); | ||
| expect(originalAgentData.totalRatings).to.equal(1); | ||
|
|
||
| // Update agent data | ||
| await registry.connect(agentOwner).setAgentData( | ||
| agentAddress, | ||
| newAgentName, | ||
| newAgentUri | ||
| expect(await agentsRegistry.serviceRegistry()).to.equal( | ||
| await newServiceRegistry.getAddress() | ||
| ); | ||
|
|
||
| // Verify reputation and ratings are preserved | ||
| const updatedAgentData = await registry.getAgentData(agentAddress); | ||
| expect(updatedAgentData.reputation).to.equal(80); | ||
| expect(updatedAgentData.totalRatings).to.equal(1); | ||
| expect(updatedAgentData.name).to.equal(newAgentName); | ||
| expect(updatedAgentData.agentUri).to.equal(newAgentUri); | ||
| }); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Emit and assert ServiceRegistryUpdated event
State change lacks an event assertion. Add event in contract and assert here.
- await agentsRegistry.connect(owner).setServiceRegistry(
- await newServiceRegistry.getAddress()
- );
+ await expect(
+ agentsRegistry.connect(owner).setServiceRegistry(await newServiceRegistry.getAddress())
+ ).to.emit(agentsRegistry, "ServiceRegistryUpdated")
+ .withArgs(await serviceRegistry.getAddress(), await newServiceRegistry.getAddress(), owner.address);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| it("Should set service registry by owner", async function () { | |
| const newServiceRegistry = await upgrades.deployProxy(ServiceRegistry, [], { | |
| initializer: "initialize", | |
| kind: "uups" | |
| }); | |
| await newServiceRegistry.waitForDeployment(); | |
| it("Should allow updating only URI", async function () { | |
| const originalAgentData = await registry.getAgentData(agentAddress); | |
| await registry.connect(agentOwner).setAgentData( | |
| agentAddress, | |
| originalAgentData.name, // Keep original name | |
| newAgentUri | |
| await agentsRegistry.connect(owner).setServiceRegistry( | |
| await newServiceRegistry.getAddress() | |
| ); | |
| const updatedAgentData = await registry.getAgentData(agentAddress); | |
| expect(updatedAgentData.name).to.equal(originalAgentData.name); | |
| expect(updatedAgentData.agentUri).to.equal(newAgentUri); | |
| }); | |
| it("Should preserve reputation and ratings after update", async function () { | |
| // Add some reputation first | |
| await registry.connect(admin).setTaskRegistry(admin); | |
| await registry.connect(admin).addRating(agentAddress, 80); | |
| const originalAgentData = await registry.getAgentData(agentAddress); | |
| expect(originalAgentData.reputation).to.equal(80); | |
| expect(originalAgentData.totalRatings).to.equal(1); | |
| // Update agent data | |
| await registry.connect(agentOwner).setAgentData( | |
| agentAddress, | |
| newAgentName, | |
| newAgentUri | |
| expect(await agentsRegistry.serviceRegistry()).to.equal( | |
| await newServiceRegistry.getAddress() | |
| ); | |
| // Verify reputation and ratings are preserved | |
| const updatedAgentData = await registry.getAgentData(agentAddress); | |
| expect(updatedAgentData.reputation).to.equal(80); | |
| expect(updatedAgentData.totalRatings).to.equal(1); | |
| expect(updatedAgentData.name).to.equal(newAgentName); | |
| expect(updatedAgentData.agentUri).to.equal(newAgentUri); | |
| }); | |
| it("Should 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() | |
| await expect( | |
| agentsRegistry.connect(owner).setServiceRegistry(await newServiceRegistry.getAddress()) | |
| ).to.emit(agentsRegistry, "ServiceRegistryUpdated") | |
| .withArgs( | |
| await serviceRegistry.getAddress(), | |
| await newServiceRegistry.getAddress(), | |
| owner.address | |
| ); | |
| expect(await agentsRegistry.serviceRegistry()).to.equal( | |
| await newServiceRegistry.getAddress() | |
| ); | |
| }); |
🤖 Prompt for AI Agents
In packages/contracts/test/AgentRegistry.test.js around lines 281 to 296, the
test updates the service registry but does not assert the expected event; add a
ServiceRegistryUpdated (or similarly named) event emission in the AgentRegistry
contract when setServiceRegistry is called, then update this test to capture the
transaction and assert that the ServiceRegistryUpdated event was emitted with
the expected new registry address and sender (owner) values using the test
framework's event assertions (e.g., expect(tx).to.emit(...).withArgs(...)).
| 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 | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
BigInt vs number in gas assertions
receipt.gasUsed is BigInt in ethers v6. Compare with BigInt to avoid type issues.
- expect(receipt.gasUsed).to.be.below(350000); // 350k gas threshold for initial service
+ expect(receipt.gasUsed).to.be.lessThan(ethers.getBigInt(350000n));
@@
- const gasIncrease = receipt2.gasUsed - receipt1.gasUsed;
- expect(gasIncrease).to.be.below(50000); // 50k gas increase threshold
+ const gasIncrease = receipt2.gasUsed - receipt1.gasUsed;
+ expect(gasIncrease).to.be.lessThan(ethers.getBigInt(50000n));📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| 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 | |
| }); | |
| 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.lessThan(ethers.getBigInt(350000n)); | |
| }); | |
| 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; | |
| const gasIncrease = receipt2.gasUsed - receipt1.gasUsed; | |
| expect(gasIncrease).to.be.lessThan(ethers.getBigInt(50000n)); | |
| }); | |
| }); |
🤖 Prompt for AI Agents
In packages/contracts/test/ServiceRegistry.test.js around lines 271 to 291, the
test compares receipt.gasUsed (a BigInt in ethers v6) to numeric literals which
causes type errors; change the numeric thresholds to BigInt values (e.g.,
350000n and 50000n) or wrap them with BigInt(), and ensure any arithmetic like
receipt2.gasUsed - receipt1.gasUsed stays in BigInt space so the expect
comparisons use BigInt operands.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
packages/contracts/contracts/AgentsRegistryUpgradeable.sol (4)
147-177: Fix potential division by zero in initial rating calculationWhen
totalRatingsis 0 (first rating), line 169-173 performs(reputation * (totalRatings - 1) + _rating) / totalRatingswhich becomes(0 * -1 + _rating) / 1. This works due to Solidity 0.8+ underflow protection converting -1 to a large number, but it's misleading and could cause issues.Apply this fix for clearer logic:
function addRating( 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 * - (agents[agent].totalRatings - 1) + - _rating) / - agents[agent].totalRatings; + // Update agent reputation + if (agents[agent].totalRatings == 0) { + agents[agent].reputation = _rating; + } else { + agents[agent].reputation = + (agents[agent].reputation * agents[agent].totalRatings + _rating) / + (agents[agent].totalRatings + 1); + } + agents[agent].totalRatings += 1; emit ReputationUpdated(agent, agents[agent].reputation); return agents[agent].reputation; }
128-139: Add event emission for successful agent migrationThe migration function should emit an event to track V1 to V2 migrations for monitoring and auditing purposes.
function migrateAgent(address agent) external { require(agents[agent].agent == address(0), "Agent already registered"); IAgentRegistryV1.AgentData memory v1AgentData = IAgentRegistryV1(agentRegistryV1).getAgentData(agent); require( msg.sender == v1AgentData.owner || msg.sender == owner(), "Not owner or agent owner" ); _createAgent(agent, v1AgentData.name, v1AgentData.agentUri, v1AgentData.owner, v1AgentData.reputation); + emit AgentMigrated(agent, v1AgentData.owner, v1AgentData.reputation); }Add the event declaration near line 89:
event AgentMigrated(address indexed agent, address indexed owner, uint256 reputation);
96-99: Emit event when service registry is updatedState-changing functions should emit events for transparency and off-chain tracking.
function setServiceRegistry(address _serviceRegistry) external onlyOwner { require(_serviceRegistry != address(0), "Invalid address"); + address oldRegistry = address(serviceRegistry); serviceRegistry = ServiceRegistryUpgradeable(_serviceRegistry); + emit ServiceRegistryUpdated(oldRegistry, _serviceRegistry); }Add the event declaration near line 89:
event ServiceRegistryUpdated(address indexed oldRegistry, address indexed newRegistry);
1-288: Add comprehensive NatSpec documentation for all public functionsPer the coding guidelines, all public functions in Solidity contracts must have complete NatSpec documentation. Several functions are missing @param and @return tags.
Functions missing complete NatSpec:
hasUserRated(lines 194-196): Missing all NatSpec tagsremoveAgent(lines 237-256): Missing @param tagsetServiceRegistry(lines 96-99): Missing complete documentationPlease add complete NatSpec documentation following this pattern:
/** * @notice Brief description of function purpose * @dev Technical details about implementation * @param paramName Description of parameter * @return Description of return value */
♻️ Duplicate comments (5)
packages/contracts/contracts/AgentsRegistryUpgradeable.sol (2)
223-234: Add registration check before ownership check for better error messagesThe function should verify agent registration before checking ownership to provide clearer error messages. Currently, non-existent agents trigger "Not the owner of the agent" which is misleading.
Apply this fix:
function setAgentData( address agent, string memory name, string memory agentUri -) external onlyAgentOwner(agent) { - require(agents[agent].agent != address(0), "Agent not registered"); +) external { + require(agents[agent].agent != address(0), "Agent not registered"); + require(agents[agent].owner == msg.sender, "Not the owner of the agent"); agents[agent].name = name; agents[agent].agentUri = agentUri; emit AgentDataUpdated(agent, name, agentUri); }
247-256: Apply consistent validation pattern for removeAgentSimilar to setAgentData, the registration check should come before the ownership check.
Apply this fix:
-function removeAgent(address agent) external onlyAgentOwner(agent) { - require(agents[agent].agent != address(0), "Agent not registered"); +function removeAgent(address agent) external { + require(agents[agent].agent != address(0), "Agent not registered"); + require(agents[agent].owner == msg.sender, "Not the owner of the agent"); address agentOwner = agents[agent].owner; // Clear agent data delete agents[agent]; emit AgentRemoved(agent, agentOwner); }packages/contracts/test/AgentRegistry.test.js (3)
30-36: Mock V1 registry type mismatch will cause migration tests to failThe test deploys a
ServiceRegistryUpgradeableas the mock V1 registry, butAgentsRegistryUpgradeableexpects anIAgentRegistryV1implementation. AnymigrateAgentcalls will fail.Create a proper mock contract:
// packages/contracts/contracts/mocks/MockAgentRegistryV1.sol // SPDX-License-Identifier: MIT pragma solidity ^0.8.22; import "../interfaces/IAgentRegistryV1.sol"; contract MockAgentRegistryV1 is IAgentRegistryV1 { mapping(address => AgentData) public agentData; function setAgentData(address agent, AgentData calldata data) external { agentData[agent] = data; } function getAgentData(address agent) external view returns (AgentData memory) { return agentData[agent]; } }Then update the test:
- // Deploy mock V1 registry for migration compatibility - mockV1Registry = await upgrades.deployProxy(ServiceRegistry, [], { - initializer: "initialize", - kind: "uups" - }); + // Deploy mock V1 registry for migration compatibility + const MockAgentRegistryV1 = await ethers.getContractFactory("MockAgentRegistryV1"); + mockV1Registry = await MockAgentRegistryV1.deploy(); await mockV1Registry.waitForDeployment();
167-175: Test expects wrong error message for non-existent agentThe test expects "Not the owner of the agent" when updating a non-existent agent, but this is misleading. After fixing the contract to check registration first, update this test.
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"); + ).to.be.revertedWith("Agent not registered"); });
333-339: Test missing event assertion for setServiceRegistryThe test should verify that the ServiceRegistryUpdated event is emitted when updating the registry.
- await agentsRegistry.connect(owner).setServiceRegistry( - await newServiceRegistry.getAddress() - ); + const tx = await agentsRegistry.connect(owner).setServiceRegistry( + await newServiceRegistry.getAddress() + ); + + await expect(tx) + .to.emit(agentsRegistry, "ServiceRegistryUpdated") + .withArgs( + await serviceRegistry.getAddress(), + await newServiceRegistry.getAddress() + );
🧹 Nitpick comments (3)
packages/contracts/contracts/AgentsRegistryUpgradeable.sol (2)
45-51: Consider adding onlyRegisteredAgent modifier for consistencyMultiple functions check
agents[agent].agent != address(0). Consider extracting this into a reusable modifier for consistency and gas optimization.+modifier onlyRegisteredAgent(address agent) { + require(agents[agent].agent != address(0), "Agent not registered"); + _; +} + modifier onlyAgentOwner(address agent) { require( agents[agent].owner == msg.sender, "Not the owner of the agent" ); _; }Then use it in
addRating,setAgentData, andremoveAgent.
160-160: Remove redundant check for rating >= 0Since
_ratingis of typeuint256, it cannot be negative. The check_rating >= 0is always true and can be removed.- require( - _rating >= 0 && _rating <= 100, - "Rating must be between 0 and 100" - ); + require( + _rating <= 100, + "Rating must be between 0 and 100" + );packages/contracts/test/AgentRegistry.test.js (1)
240-243: Improve invalid rating test coverageThe test only checks ratings > 100. Consider testing edge cases more thoroughly.
it("Should reject invalid ratings", async function () { await expect( agentsRegistry.connect(addr2).addRating(agentAddr1.address, 101) ).to.be.revertedWith("Rating must be between 0 and 100"); + + // Test very large values + await expect( + agentsRegistry.connect(addr2).addRating(agentAddr1.address, 1000) + ).to.be.revertedWith("Rating must be between 0 and 100"); + + // Test max uint256 (if applicable) + await expect( + agentsRegistry.connect(addr2).addRating(agentAddr1.address, ethers.MaxUint256) + ).to.be.revertedWith("Rating must be between 0 and 100"); });
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (2)
packages/contracts/contracts/AgentsRegistryUpgradeable.sol(7 hunks)packages/contracts/test/AgentRegistry.test.js(1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
packages/contracts/contracts/**/*.sol
📄 CodeRabbit inference engine (packages/contracts/.cursor/rules/project-intelligence.mdc)
packages/contracts/contracts/**/*.sol: All public functions in Solidity contracts must have complete NatSpec documentation comments
Custom errors with descriptive names should be used for error handling in Solidity contracts instead of string messages
All state changes in Solidity contracts must emit appropriate events
Follow OpenZeppelin security patterns in all Solidity contracts
Files:
packages/contracts/contracts/AgentsRegistryUpgradeable.sol
packages/contracts/**/*.sol
📄 CodeRabbit inference engine (packages/contracts/.cursor/rules/role.mdc)
packages/contracts/**/*.sol: Use explicit function visibility modifiers and appropriate natspec comments.
Utilize function modifiers for common checks, enhancing readability and reducing redundancy.
Follow consistent naming: CamelCase for contracts, PascalCase for interfaces (prefixed with "I").
Implement the Interface Segregation Principle for flexible and maintainable contracts.
Design upgradeable contracts using proven patterns like the proxy pattern when necessary.
Implement comprehensive events for all significant state changes.
Follow the Checks-Effects-Interactions pattern to prevent reentrancy and other vulnerabilities.
Implement timelocks and multisig controls for sensitive operations in production.
Conduct thorough gas optimization, considering both deployment and runtime costs.
Use OpenZeppelin's AccessControl for fine-grained permissions.
Use Solidity 0.8.0+ for built-in overflow/underflow protection.
Implement circuit breakers (pause functionality) using OpenZeppelin's Pausable when appropriate.
Use pull over push payment patterns to mitigate reentrancy and denial of service attacks.
Implement rate limiting for sensitive functions to prevent abuse.
Use OpenZeppelin's SafeERC20 for interacting with ERC20 tokens.
Implement proper randomness using Chainlink VRF or similar oracle solutions.
Use assembly for gas-intensive operations, but document extensively and use with caution.
Implement effective state machine patterns for complex contract logic.
Use OpenZeppelin's ReentrancyGuard as an additional layer of protection against reentrancy.
Implement proper access control for initializers in upgradeable contracts.
Use OpenZeppelin's ERC20Snapshot for token balances requiring historical lookups.
Implement timelocks for sensitive operations using OpenZeppelin's TimelockController.
Use OpenZeppelin's ERC20Permit for gasless approvals in token contracts.
Implement proper slippage protection for DEX-like functionalities.
Use OpenZeppelin's ERC20Votes for governa...
Files:
packages/contracts/contracts/AgentsRegistryUpgradeable.sol
🧠 Learnings (4)
📚 Learning: 2025-08-01T07:27:26.601Z
Learnt from: CR
PR: ensemble-codes/ensemble-framework#0
File: packages/contracts/.cursor/rules/project-intelligence.mdc:0-0
Timestamp: 2025-08-01T07:27:26.601Z
Learning: Applies to packages/contracts/test/*Registry*.sol : All registry contracts must have extensive test coverage, including edge cases and integration scenarios
Applied to files:
packages/contracts/test/AgentRegistry.test.js
📚 Learning: 2025-08-01T07:27:26.601Z
Learnt from: CR
PR: ensemble-codes/ensemble-framework#0
File: packages/contracts/.cursor/rules/project-intelligence.mdc:0-0
Timestamp: 2025-08-01T07:27:26.601Z
Learning: Applies to packages/contracts/test/**/*{Integration,EnsembleCredits,Registry}*.sol : Integration testing must verify interaction between EnsembleCredits and registry contracts
Applied to files:
packages/contracts/test/AgentRegistry.test.js
📚 Learning: 2025-08-01T07:27:59.616Z
Learnt from: CR
PR: ensemble-codes/ensemble-framework#0
File: packages/contracts/.cursor/rules/role.mdc:0-0
Timestamp: 2025-08-01T07:27:59.616Z
Learning: Applies to packages/contracts/**/*.sol : Design upgradeable contracts using proven patterns like the proxy pattern when necessary.
Applied to files:
packages/contracts/contracts/AgentsRegistryUpgradeable.sol
📚 Learning: 2025-08-01T07:27:59.616Z
Learnt from: CR
PR: ensemble-codes/ensemble-framework#0
File: packages/contracts/.cursor/rules/role.mdc:0-0
Timestamp: 2025-08-01T07:27:59.616Z
Learning: Applies to packages/contracts/**/*.sol : Implement proper access control for initializers in upgradeable contracts.
Applied to files:
packages/contracts/contracts/AgentsRegistryUpgradeable.sol
🧬 Code graph analysis (1)
packages/contracts/test/AgentRegistry.test.js (2)
packages/contracts/scripts/deploy.js (5)
AgentsRegistry(24-24)ServiceRegistry(8-8)agentsRegistry(25-28)serviceRegistry(9-12)mockV1Registry(17-20)packages/contracts/test/ServiceRegistry.test.js (8)
ServiceRegistry(5-5)owner(7-7)ZERO_ADDRESS(13-13)implementationAddress(35-35)tx(51-51)tx(73-73)tx(153-153)tx(273-273)
| // Clear agent data | ||
| delete agents[agent]; | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Clear rating history when removing agent
When an agent is removed, the rating history in the hasRated mapping is not cleared. This could prevent users from rating a re-registered agent.
Consider either:
- Document that rating history persists across re-registrations (intended behavior)
- Add a mechanism to clear rating history on removal
If you want to clear history, additional state tracking would be needed since you can't iterate over all raters. Consider adding an agent version/nonce that increments on each registration.
|
|
||
| it("Should handle agent address same as owner address", async function () { | ||
| await registry.connect(agentOwner).registerAgent( | ||
| agentOwner.address, | ||
| "Self Agent", | ||
| agentUri | ||
| describe("3. Agent Data Management", function () { | ||
| beforeEach(async function () { | ||
| await agentsRegistry.connect(addr1).registerAgent( | ||
| agentAddr1.address, | ||
| AGENT_NAME_1, | ||
| AGENT_URI_1 | ||
| ); | ||
|
|
||
| const agentData = await registry.getAgentData(agentOwner.address); | ||
| expect(agentData.owner).to.equal(agentOwner.address); | ||
| expect(agentData.agent).to.equal(agentOwner.address); | ||
| }); | ||
|
|
||
| it("Should handle very long strings", async function () { | ||
| const longName = "A".repeat(1000); | ||
| const longUri = "https://".concat("very-long-uri-".repeat(100)); | ||
| it("Should update agent data by owner", async function () { | ||
| const newName = "Updated Agent Name"; | ||
| const newUri = "ipfs://QmUpdatedHash/metadata.json"; | ||
|
|
||
| await registry.connect(agentOwner).registerAgent( | ||
| agentAddress, | ||
| longName, | ||
| longUri | ||
| const tx = await agentsRegistry.connect(addr1).setAgentData( | ||
| agentAddr1.address, | ||
| newName, | ||
| newUri | ||
| ); | ||
|
|
||
| const agentData = await registry.getAgentData(agentAddress); | ||
| expect(agentData.name).to.equal(longName); | ||
| expect(agentData.agentUri).to.equal(longUri); | ||
| }); | ||
| await expect(tx) | ||
| .to.emit(agentsRegistry, "AgentDataUpdated") | ||
| .withArgs(agentAddr1.address, newName, newUri); | ||
|
|
||
| it("Should not create any proposals when using registerAgent", async function () { | ||
| await registry.connect(agentOwner).registerAgent( | ||
| agentAddress, | ||
| "Simple Agent", | ||
| agentUri | ||
| ); | ||
| const agentData = await agentsRegistry.getAgentData(agentAddr1.address); | ||
| expect(agentData.name).to.equal(newName); | ||
| expect(agentData.agentUri).to.equal(newUri); | ||
| }); | ||
|
|
||
| // Since proposals start at ID 1, checking if proposal 1 exists should return default values | ||
| const proposal = await registry.getProposal(1); | ||
| expect(proposal.issuer).to.equal(ethers.ZeroAddress); | ||
| expect(proposal.serviceName).to.equal(""); | ||
| expect(proposal.price).to.equal(0); | ||
| expect(proposal.proposalId).to.equal(0); | ||
| expect(proposal.isActive).to.equal(false); | ||
| }); | ||
| }) | ||
|
|
||
| describe('#registerAgentWithService', () => { | ||
| this.beforeEach(async function () { | ||
| await serviceRegistry.registerService("Service1", "Category1", "Description1"); | ||
| }) | ||
| it("Should not register an agent if the service is not registered", async function () { | ||
| it("Should fail to update agent data by non-owner", async function () { | ||
| await expect( | ||
| registry.connect(agentOwner).registerAgentWithService( | ||
| agentAddress, | ||
| "Service Agent", | ||
| agentUri, | ||
| "NonExistentService", | ||
| ethers.parseEther("0.01"), | ||
| ethers.ZeroAddress | ||
| agentsRegistry.connect(addr2).setAgentData( | ||
| agentAddr1.address, | ||
| "New Name", | ||
| "New URI" | ||
| ) | ||
| ).to.be.revertedWith("Service not registered"); | ||
| ).to.be.revertedWith("Not the owner of the agent"); | ||
| }); | ||
|
|
||
| it("Should register new agent", async function () { | ||
| const request = registry.connect(agentOwner).registerAgentWithService( | ||
| agentAddress, | ||
| "Service Agent", | ||
| agentUri, | ||
| "Service1", | ||
| ethers.parseEther("0.01"), | ||
| ethers.ZeroAddress | ||
| ); | ||
|
|
||
| await expect(request) | ||
| .to.emit(registry, "AgentRegistered") | ||
| .withArgs(agentAddress, agentOwner, "Service Agent", agentUri) | ||
| .to.emit(registry, "ProposalAdded") | ||
| .withArgs(agentAddress, 1, "Service1", ethers.parseEther("0.01"), ethers.ZeroAddress); | ||
|
|
||
| const agentData = await registry.getAgentData(agentAddress); | ||
|
|
||
| expect(agentData.name).to.equal("Service Agent"); | ||
| expect(agentData.agentUri).to.equal(agentUri); | ||
| expect(agentData.owner).to.equal(agentOwner.address); | ||
| expect(agentData.agent).to.equal(agentAddress); | ||
| expect(agentData.reputation).to.equal(0); | ||
|
|
||
| const proposalId = 1; | ||
| const proposal = await registry.getProposal(proposalId); | ||
| expect(proposal.issuer).to.equal(agentAddress); | ||
| expect(proposal.serviceName).to.equal("Service1"); | ||
| expect(proposal.price).to.equal(ethers.parseEther("0.01")); | ||
| expect(proposal.proposalId).to.equal(proposalId); | ||
| }); | ||
|
|
||
| it("Should not register the same agent twice", async function () { | ||
| await registry.connect(agentOwner).registerAgentWithService( | ||
| agentAddress, | ||
| "Service Agent", | ||
| agentUri, | ||
| "Service1", | ||
| ethers.parseEther("0.01"), | ||
| ethers.ZeroAddress | ||
| ); | ||
|
|
||
| it("Should fail to update non-existent agent", async function () { | ||
| await expect( | ||
| registry.connect(agentOwner).registerAgentWithService( | ||
| agentAddress, | ||
| "Service Agent", | ||
| agentUri, | ||
| "Service1", | ||
| ethers.parseEther("0.01"), | ||
| ethers.ZeroAddress | ||
| agentsRegistry.connect(addr1).setAgentData( | ||
| agentAddr2.address, | ||
| "New Name", | ||
| "New URI" | ||
| ) | ||
| ).to.be.revertedWith("Agent already registered"); | ||
| ).to.be.revertedWith("Not the owner of the agent"); | ||
| }); | ||
| }) | ||
|
|
||
|
|
||
| describe('#Proposals', () => { | ||
| }); | ||
|
|
||
| describe("4. Agent Reputation System", function () { | ||
| beforeEach(async function () { | ||
| await registry.connect(agentOwner).registerAgentWithService( | ||
| agentAddress, | ||
| "Service Agent", | ||
| agentUri, | ||
| "Service1", | ||
| ethers.parseEther("0.01"), | ||
| ethers.ZeroAddress | ||
| await agentsRegistry.connect(addr1).registerAgent( | ||
| agentAddr1.address, | ||
| AGENT_NAME_1, | ||
| AGENT_URI_1 | ||
| ); | ||
| }); | ||
|
|
||
| it("Should add rating to agent", async function () { | ||
| const rating = 85; | ||
| const tx = await agentsRegistry.connect(addr2).addRating(agentAddr1.address, rating); | ||
|
|
||
| it("Should add a proposal", async function () { | ||
| await registry.connect(agentOwner).addProposal(agentAddress, "Service1", ethers.parseEther("0.02"), ethers.ZeroAddress); | ||
| const proposalId = 2 | ||
| expect(await registry.getProposal(proposalId)).to.deep.equal([ | ||
| agentAddress.address, | ||
| "Service1", | ||
| ethers.parseEther("0.02"), | ||
| ethers.ZeroAddress, | ||
| proposalId, | ||
| true | ||
| ]); | ||
| }); | ||
|
|
||
| it("Should not add a proposal if service is not registered", async function () { | ||
| await expect(registry.connect(agentOwner).addProposal(agentAddress, "NonExistentService", ethers.parseEther("0.02"), ethers.ZeroAddress)) | ||
| .to.be.revertedWith("Service not registered"); | ||
| }); | ||
|
|
||
| it("Should remove a proposal", async function () { | ||
|
|
||
| await registry.connect(agentOwner).addProposal(agentAddress, "Service1", ethers.parseEther("0.02"), ethers.ZeroAddress); | ||
|
|
||
| await registry.connect(agentOwner).removeProposal(agentAddress, 2); | ||
|
|
||
| expect(await registry.getProposal(2)).to.deep.equal([ | ||
| ethers.ZeroAddress, | ||
| "", | ||
| 0, | ||
| ethers.ZeroAddress, | ||
| 0, | ||
| false | ||
| ]); | ||
| await expect(tx) | ||
| .to.emit(agentsRegistry, "ReputationUpdated") | ||
| .withArgs(agentAddr1.address, rating); | ||
|
|
||
| const reputation = await agentsRegistry.getReputation(agentAddr1.address); | ||
| expect(reputation).to.equal(rating); | ||
|
|
||
| await registry.connect(agentOwner).removeProposal(agentAddress, 1); | ||
|
|
||
| expect(await registry.getProposal(1)).to.deep.equal([ | ||
| ethers.ZeroAddress, | ||
| "", | ||
| 0, | ||
| ethers.ZeroAddress, | ||
| 0, | ||
| false | ||
| ]); | ||
| const agentData = await agentsRegistry.getAgentData(agentAddr1.address); | ||
| expect(agentData.totalRatings).to.equal(1); | ||
|
|
||
| // Check that the user has rated | ||
| expect(await agentsRegistry.hasUserRated(agentAddr1.address, addr2.address)).to.be.true; | ||
| }); | ||
| }); | ||
|
|
||
|
|
||
| describe('#Reputation', () => { | ||
|
|
||
| beforeEach(async () => { | ||
| await registry.connect(agentOwner).registerAgentWithService( | ||
| agentAddress, | ||
| "Service Agent", | ||
| agentUri, | ||
| "Service1", | ||
| ethers.parseEther("0.01"), | ||
| ethers.ZeroAddress | ||
| ); | ||
| }) | ||
| it("Should only allow taskRegistry to add a rating", async function () { | ||
| // Attempt to add a rating from a different address | ||
| await expect(registry.connect(agentOwner).addRating(agentAddress, 50)).to.be.revertedWith("Not the TaskRegistry contract"); | ||
|
|
||
| await registry.connect(admin).setTaskRegistry(admin); | ||
|
|
||
| await expect(registry.connect(admin).addRating(agentAddress, 50)).to.not.be.reverted; | ||
| it("Should prevent duplicate ratings from same user", async function () { | ||
| // First rating should succeed | ||
| await agentsRegistry.connect(addr1).addRating(agentAddr1.address, 80); | ||
|
|
||
| // Second rating from same user should fail | ||
| await expect( | ||
| agentsRegistry.connect(addr1).addRating(agentAddr1.address, 90) | ||
| ).to.be.revertedWith("User has already rated this agent"); | ||
|
|
||
| // Check that only one rating was recorded | ||
| const agentData = await agentsRegistry.getAgentData(agentAddr1.address); | ||
| expect(agentData.totalRatings).to.equal(1); | ||
| expect(agentData.reputation).to.equal(80); | ||
| }); | ||
|
|
||
| it("Should add a rating", async function () { | ||
| await registry.connect(admin).setTaskRegistry(admin); | ||
| it("Should calculate average reputation correctly with different users", async function () { | ||
| // Different users rating the same agent | ||
| await agentsRegistry.connect(addr1).addRating(agentAddr1.address, 80); | ||
| await agentsRegistry.connect(addr2).addRating(agentAddr1.address, 90); | ||
| await agentsRegistry.connect(owner).addRating(agentAddr1.address, 100); | ||
|
|
||
| await expect(registry.connect(admin).addRating(agentAddress, 50)) | ||
| .to.emit(registry, "ReputationUpdated") | ||
| .withArgs(agentAddress, 50); | ||
| let agentData = await registry.getAgentData(agentAddress); | ||
| expect(agentData.reputation).to.equal(50); | ||
| expect(agentData.totalRatings).to.equal(1); | ||
| expect(await registry.getReputation(agentAddress)).to.equal(50); | ||
|
|
||
| await expect(registry.connect(admin).addRating(agentAddress, 100)) | ||
| .to.emit(registry, "ReputationUpdated") | ||
| .withArgs(agentAddress, 75); | ||
|
|
||
| agentData = await registry.getAgentData(agentAddress); | ||
| expect(agentData.reputation).to.equal(75); | ||
| expect(agentData.totalRatings).to.equal(2); | ||
| const reputation = await agentsRegistry.getReputation(agentAddr1.address); | ||
| expect(reputation).to.equal(90); // (80 + 90 + 100) / 3 = 90 | ||
|
|
||
| await expect(registry.connect(admin).addRating(agentAddress, 30)) | ||
| .to.emit(registry, "ReputationUpdated") | ||
| .withArgs(agentAddress, 60); | ||
|
|
||
| agentData = await registry.getAgentData(agentAddress); | ||
| expect(agentData.reputation).to.equal(60); | ||
| const agentData = await agentsRegistry.getAgentData(agentAddr1.address); | ||
| expect(agentData.totalRatings).to.equal(3); | ||
| }) | ||
| }) | ||
|
|
||
| describe('#MigrateAgent', () => { | ||
| this.beforeEach(async function () { | ||
| await serviceRegistryV1.registerService("Service1", "Category1", "Description1"); | ||
| }) | ||
|
|
||
| // FIXME: figure out why getAgentData is not returning the correct data | ||
| it("Should migrate an agent to a new registry", async function () { | ||
| await agentRegistryV1.connect(agentOwner).registerAgent( | ||
| agentAddress, | ||
| "Service Agent", | ||
| agentUri, | ||
| "Service1", | ||
| ethers.parseEther("0.01") | ||
| ); | ||
| }); | ||
|
|
||
| it("Should reject rating for non-existent agent", async function () { | ||
| await expect( | ||
| registry.connect(eveAddress).migrateAgent(agentAddress) | ||
| ).to.be.revertedWith("Not owner or agent owner"); | ||
| agentsRegistry.connect(addr1).addRating(agentAddr2.address, 80) | ||
| ).to.be.revertedWith("Agent not registered"); | ||
| }); | ||
|
|
||
| await registry.migrateAgent(agentAddress) | ||
| it("Should reject invalid ratings", async function () { | ||
| await expect( | ||
| agentsRegistry.connect(addr2).addRating(agentAddr1.address, 101) | ||
| ).to.be.revertedWith("Rating must be between 0 and 100"); | ||
| }); | ||
|
|
||
| const agentData = await registry.getAgentData(agentAddress); | ||
| it("Should handle edge case ratings", async function () { | ||
| await agentsRegistry.connect(addr1).addRating(agentAddr1.address, 0); | ||
| let reputation = await agentsRegistry.getReputation(agentAddr1.address); | ||
| expect(reputation).to.equal(0); | ||
|
|
||
| expect(agentData.name).to.equal("Service Agent"); | ||
| expect(agentData.agentUri).to.equal(agentUri); | ||
| expect(agentData.owner).to.equal(agentOwner.address); | ||
| expect(agentData.agent).to.equal(agentAddress); | ||
| expect(agentData.reputation).to.equal(0); | ||
| expect(agentData.totalRatings).to.equal(0); | ||
| await agentsRegistry.connect(addr2).addRating(agentAddr1.address, 100); | ||
| reputation = await agentsRegistry.getReputation(agentAddr1.address); | ||
| expect(reputation).to.equal(50); // (0 + 100) / 2 = 50 | ||
| }); | ||
| }) | ||
|
|
||
| describe('#SetAgentData', () => { | ||
| const newAgentName = "Updated Agent Name"; | ||
| const newAgentUri = "https://ipfs.io/ipfs/updated-hash"; | ||
| it("Should track rating status correctly", async function () { | ||
| // Check initial state | ||
| expect(await agentsRegistry.hasUserRated(agentAddr1.address, addr1.address)).to.be.false; | ||
| expect(await agentsRegistry.hasUserRated(agentAddr1.address, addr2.address)).to.be.false; | ||
|
|
||
| // Add rating from addr1 | ||
| await agentsRegistry.connect(addr1).addRating(agentAddr1.address, 75); | ||
|
|
||
| // Check updated state | ||
| expect(await agentsRegistry.hasUserRated(agentAddr1.address, addr1.address)).to.be.true; | ||
| expect(await agentsRegistry.hasUserRated(agentAddr1.address, addr2.address)).to.be.false; | ||
|
|
||
| // Add rating from addr2 | ||
| await agentsRegistry.connect(addr2).addRating(agentAddr1.address, 85); | ||
|
|
||
| // Check final state | ||
| expect(await agentsRegistry.hasUserRated(agentAddr1.address, addr1.address)).to.be.true; | ||
| expect(await agentsRegistry.hasUserRated(agentAddr1.address, addr2.address)).to.be.true; | ||
| }); | ||
| }); | ||
|
|
||
| describe("5. Agent Removal", function () { | ||
| beforeEach(async function () { | ||
| await registry.connect(agentOwner).registerAgentWithService( | ||
| agentAddress, | ||
| "Service Agent", | ||
| agentUri, | ||
| "Service1", | ||
| ethers.parseEther("0.01"), | ||
| ethers.ZeroAddress | ||
| await agentsRegistry.connect(addr1).registerAgent( | ||
| agentAddr1.address, | ||
| AGENT_NAME_1, | ||
| AGENT_URI_1 | ||
| ); | ||
| }); | ||
|
|
||
| it("Should successfully update agent data", async function () { | ||
| const updateTx = registry.connect(agentOwner).setAgentData( | ||
| agentAddress, | ||
| newAgentName, | ||
| newAgentUri | ||
| ); | ||
| it("Should remove agent by owner", async function () { | ||
| const tx = await agentsRegistry.connect(addr1).removeAgent(agentAddr1.address); | ||
|
|
||
| await expect(updateTx) | ||
| .to.emit(registry, "AgentDataUpdated") | ||
| .withArgs(agentAddress, newAgentName, newAgentUri); | ||
| await expect(tx) | ||
| .to.emit(agentsRegistry, "AgentRemoved") | ||
| .withArgs(agentAddr1.address, addr1.address); | ||
|
|
||
| const agentData = await registry.getAgentData(agentAddress); | ||
| expect(agentData.name).to.equal(newAgentName); | ||
| expect(agentData.agentUri).to.equal(newAgentUri); | ||
|
|
||
| // Verify other fields remain unchanged | ||
| expect(agentData.owner).to.equal(agentOwner.address); | ||
| expect(agentData.agent).to.equal(agentAddress); | ||
| expect(agentData.reputation).to.equal(0); | ||
| expect(agentData.totalRatings).to.equal(0); | ||
| const agentData = await agentsRegistry.getAgentData(agentAddr1.address); | ||
| expect(agentData.agent).to.equal(ZERO_ADDRESS); | ||
| expect(agentData.name).to.equal(""); | ||
| }); | ||
|
|
||
| it("Should not allow non-owner to update agent data", async function () { | ||
| const [, , , unauthorizedUser] = await ethers.getSigners(); | ||
|
|
||
| it("Should fail to remove agent by non-owner", async function () { | ||
| await expect( | ||
| registry.connect(unauthorizedUser).setAgentData( | ||
| agentAddress, | ||
| newAgentName, | ||
| newAgentUri | ||
| ) | ||
| agentsRegistry.connect(addr2).removeAgent(agentAddr1.address) | ||
| ).to.be.revertedWith("Not the owner of the agent"); | ||
| }); | ||
|
|
||
| it("Should not allow updating unregistered agent", async function () { | ||
| const [, , , , unregisteredAgent] = await ethers.getSigners(); | ||
|
|
||
| it("Should fail to remove non-existent agent", async function () { | ||
| await expect( | ||
| registry.connect(unregisteredAgent).setAgentData( | ||
| unregisteredAgent.address, | ||
| newAgentName, | ||
| newAgentUri | ||
| ) | ||
| agentsRegistry.connect(addr1).removeAgent(agentAddr2.address) | ||
| ).to.be.revertedWith("Not the owner of the agent"); | ||
| }); | ||
|
|
||
| it("Should allow updating with empty strings", async function () { | ||
| const emptyName = ""; | ||
| const emptyUri = ""; | ||
| it("Should allow re-registration after removal", async function () { | ||
| await agentsRegistry.connect(addr1).removeAgent(agentAddr1.address); | ||
|
|
||
| const updateTx = registry.connect(agentOwner).setAgentData( | ||
| agentAddress, | ||
| emptyName, | ||
| emptyUri | ||
| // Should be able to register the same agent address again | ||
| await agentsRegistry.connect(addr2).registerAgent( | ||
| agentAddr1.address, | ||
| AGENT_NAME_2, | ||
| AGENT_URI_2 | ||
| ); | ||
|
|
||
| await expect(updateTx) | ||
| .to.emit(registry, "AgentDataUpdated") | ||
| .withArgs(agentAddress, emptyName, emptyUri); | ||
|
|
||
| const agentData = await registry.getAgentData(agentAddress); | ||
| expect(agentData.name).to.equal(emptyName); | ||
| expect(agentData.agentUri).to.equal(emptyUri); | ||
| const agentData = await agentsRegistry.getAgentData(agentAddr1.address); | ||
| expect(agentData.owner).to.equal(addr2.address); | ||
| expect(agentData.name).to.equal(AGENT_NAME_2); | ||
| }); | ||
| }); | ||
|
|
||
| it("Should allow updating only name", async function () { | ||
| const originalAgentData = await registry.getAgentData(agentAddress); | ||
|
|
||
| await registry.connect(agentOwner).setAgentData( | ||
| agentAddress, | ||
| newAgentName, | ||
| originalAgentData.agentUri // Keep original URI | ||
| ); | ||
| describe("6. Service Registry Management", function () { | ||
| it("Should set service registry by owner", async function () { | ||
| const newServiceRegistry = await upgrades.deployProxy(ServiceRegistry, [], { | ||
| initializer: "initialize", | ||
| kind: "uups" | ||
| }); | ||
| await newServiceRegistry.waitForDeployment(); | ||
|
|
||
| const updatedAgentData = await registry.getAgentData(agentAddress); | ||
| expect(updatedAgentData.name).to.equal(newAgentName); | ||
| expect(updatedAgentData.agentUri).to.equal(originalAgentData.agentUri); | ||
| }); | ||
|
|
||
| it("Should allow updating only URI", async function () { | ||
| const originalAgentData = await registry.getAgentData(agentAddress); | ||
|
|
||
| await registry.connect(agentOwner).setAgentData( | ||
| agentAddress, | ||
| originalAgentData.name, // Keep original name | ||
| newAgentUri | ||
| await agentsRegistry.connect(owner).setServiceRegistry( | ||
| await newServiceRegistry.getAddress() | ||
| ); | ||
|
|
||
| const updatedAgentData = await registry.getAgentData(agentAddress); | ||
| expect(updatedAgentData.name).to.equal(originalAgentData.name); | ||
| expect(updatedAgentData.agentUri).to.equal(newAgentUri); | ||
| }); | ||
|
|
||
| it("Should preserve reputation and ratings after update", async function () { | ||
| // Add some reputation first | ||
| await registry.connect(admin).setTaskRegistry(admin); | ||
| await registry.connect(admin).addRating(agentAddress, 80); | ||
|
|
||
| const originalAgentData = await registry.getAgentData(agentAddress); | ||
| expect(originalAgentData.reputation).to.equal(80); | ||
| expect(originalAgentData.totalRatings).to.equal(1); | ||
|
|
||
| // Update agent data | ||
| await registry.connect(agentOwner).setAgentData( | ||
| agentAddress, | ||
| newAgentName, | ||
| newAgentUri | ||
| expect(await agentsRegistry.serviceRegistry()).to.equal( | ||
| await newServiceRegistry.getAddress() | ||
| ); | ||
|
|
||
| // Verify reputation and ratings are preserved | ||
| const updatedAgentData = await registry.getAgentData(agentAddress); | ||
| expect(updatedAgentData.reputation).to.equal(80); | ||
| expect(updatedAgentData.totalRatings).to.equal(1); | ||
| expect(updatedAgentData.name).to.equal(newAgentName); | ||
| expect(updatedAgentData.agentUri).to.equal(newAgentUri); | ||
| }); | ||
|
|
||
| it("Should allow multiple updates", async function () { | ||
| // First update | ||
| await registry.connect(agentOwner).setAgentData( | ||
| agentAddress, | ||
| "First Update", | ||
| "https://first-update.com" | ||
| ); | ||
|
|
||
| let agentData = await registry.getAgentData(agentAddress); | ||
| expect(agentData.name).to.equal("First Update"); | ||
| expect(agentData.agentUri).to.equal("https://first-update.com"); | ||
|
|
||
| // Second update | ||
| await registry.connect(agentOwner).setAgentData( | ||
| agentAddress, | ||
| "Second Update", | ||
| "https://second-update.com" | ||
| ); | ||
| it("Should fail to set service registry by non-owner", async function () { | ||
| await expect( | ||
| agentsRegistry.connect(addr1).setServiceRegistry(addr2.address) | ||
| ).to.be.reverted; | ||
| }); | ||
|
|
||
| agentData = await registry.getAgentData(agentAddress); | ||
| expect(agentData.name).to.equal("Second Update"); | ||
| expect(agentData.agentUri).to.equal("https://second-update.com"); | ||
| it("Should fail to set zero address as service registry", async function () { | ||
| await expect( | ||
| agentsRegistry.connect(owner).setServiceRegistry(ZERO_ADDRESS) | ||
| ).to.be.revertedWith("Invalid address"); | ||
| }); | ||
| }); | ||
|
|
||
| describe('#RemoveAgent', () => { | ||
| describe("7. Access Control", function () { | ||
| beforeEach(async function () { | ||
| await serviceRegistry.registerService("RemoveService", "Category1", "Description1"); | ||
| await registry.connect(agentOwner).registerAgentWithService( | ||
| agentAddress, | ||
| "Service Agent", | ||
| agentUri, | ||
| "RemoveService", | ||
| ethers.parseEther("0.01"), | ||
| ethers.ZeroAddress | ||
| await agentsRegistry.connect(addr1).registerAgent( | ||
| agentAddr1.address, | ||
| AGENT_NAME_1, | ||
| AGENT_URI_1 | ||
| ); | ||
| }); | ||
|
|
||
| it("Should successfully remove an agent", async function () { | ||
| const removeTx = registry.connect(agentOwner).removeAgent(agentAddress); | ||
|
|
||
| await expect(removeTx) | ||
| .to.emit(registry, "AgentRemoved") | ||
| .withArgs(agentAddress, agentOwner.address) | ||
| .to.emit(registry, "ProposalRemoved") | ||
| .withArgs(agentAddress, 1); | ||
|
|
||
| // Verify agent data is cleared | ||
| const agentData = await registry.getAgentData(agentAddress); | ||
| expect(agentData.agent).to.equal(ethers.ZeroAddress); | ||
| expect(agentData.owner).to.equal(ethers.ZeroAddress); | ||
| expect(agentData.name).to.equal(""); | ||
| expect(agentData.agentUri).to.equal(""); | ||
| expect(agentData.reputation).to.equal(0); | ||
| expect(agentData.totalRatings).to.equal(0); | ||
|
|
||
| // Verify proposal is removed | ||
| const proposal = await registry.getProposal(1); | ||
| expect(proposal.issuer).to.equal(ethers.ZeroAddress); | ||
| expect(proposal.serviceName).to.equal(""); | ||
| expect(proposal.isActive).to.equal(false); | ||
| }); | ||
|
|
||
| it("Should not allow non-owner to remove agent", async function () { | ||
| const [, , , unauthorizedUser] = await ethers.getSigners(); | ||
|
|
||
| it("Should enforce onlyAgentOwner modifier", async function () { | ||
| // Try to update agent data as non-owner | ||
| await expect( | ||
| registry.connect(unauthorizedUser).removeAgent(agentAddress) | ||
| agentsRegistry.connect(addr2).setAgentData( | ||
| agentAddr1.address, | ||
| "New Name", | ||
| "New URI" | ||
| ) | ||
| ).to.be.revertedWith("Not the owner of the agent"); | ||
| }); | ||
|
|
||
| it("Should not allow removing non-existent agent", async function () { | ||
| const [, , , , unregisteredAgent] = await ethers.getSigners(); | ||
|
|
||
| // Try to remove agent as non-owner | ||
| await expect( | ||
| registry.connect(unregisteredAgent).removeAgent(unregisteredAgent.address) | ||
| agentsRegistry.connect(addr2).removeAgent(agentAddr1.address) | ||
| ).to.be.revertedWith("Not the owner of the agent"); | ||
| }); | ||
|
|
||
| it("Should allow re-registration after removing", async function () { | ||
| // Remove agent | ||
| await registry.connect(agentOwner).removeAgent(agentAddress); | ||
|
|
||
| // Re-register the same agent | ||
| const reregisterTx = registry.connect(agentOwner).registerAgent( | ||
| agentAddress, | ||
| "Re-registered Agent", | ||
| "https://new-uri.com" | ||
| ); | ||
|
|
||
| await expect(reregisterTx) | ||
| .to.emit(registry, "AgentRegistered") | ||
| .withArgs(agentAddress, agentOwner.address, "Re-registered Agent", "https://new-uri.com"); | ||
|
|
||
| const agentData = await registry.getAgentData(agentAddress); | ||
| expect(agentData.name).to.equal("Re-registered Agent"); | ||
| expect(agentData.agentUri).to.equal("https://new-uri.com"); | ||
| expect(agentData.owner).to.equal(agentOwner.address); | ||
| expect(agentData.agent).to.equal(agentAddress); | ||
| expect(agentData.reputation).to.equal(0); | ||
| expect(agentData.totalRatings).to.equal(0); | ||
| it("Should enforce onlyOwner modifier", async function () { | ||
| await expect( | ||
| agentsRegistry.connect(addr1).setServiceRegistry(addr2.address) | ||
| ).to.be.reverted; | ||
| }); | ||
| }); | ||
|
|
||
| it("Should remove multiple proposals when removing", async function () { | ||
| // Add another proposal | ||
| await registry.connect(agentOwner).addProposal( | ||
| agentAddress, | ||
| "RemoveService", | ||
| ethers.parseEther("0.02"), | ||
| ethers.ZeroAddress | ||
| ); | ||
|
|
||
| const removeTx = registry.connect(agentOwner).removeAgent(agentAddress); | ||
|
|
||
| await expect(removeTx) | ||
| .to.emit(registry, "AgentRemoved") | ||
| .withArgs(agentAddress, agentOwner.address) | ||
| .to.emit(registry, "ProposalRemoved") | ||
| .withArgs(agentAddress, 2); | ||
|
|
||
| // Verify that only the active proposal was removed (proposal 1 was already removed) | ||
| const proposal1 = await registry.getProposal(1); | ||
| const proposal2 = await registry.getProposal(2); | ||
|
|
||
| // Proposal 1 should remain in its already-removed state | ||
| expect(proposal1.issuer).to.equal(ethers.ZeroAddress); | ||
| expect(proposal1.isActive).to.equal(false); | ||
|
|
||
| // Proposal 2 should now be removed by remove | ||
| expect(proposal2.issuer).to.equal(ethers.ZeroAddress); | ||
| expect(proposal2.isActive).to.equal(false); | ||
| }); | ||
|
|
||
| it("Should preserve reputation history for other agents", async function () { | ||
| // Register another agent | ||
| const [, , , , secondAgent] = await ethers.getSigners(); | ||
| await registry.connect(agentOwner).registerAgent( | ||
| secondAgent.address, | ||
| "Second Agent", | ||
| agentUri | ||
| describe("8. Data Retrieval", function () { | ||
| beforeEach(async function () { | ||
| await agentsRegistry.connect(addr1).registerAgent( | ||
| agentAddr1.address, | ||
| AGENT_NAME_1, | ||
| AGENT_URI_1 | ||
| ); | ||
|
|
||
| // Add reputation to both agents | ||
| await registry.connect(admin).setTaskRegistry(admin); | ||
| await registry.connect(admin).addRating(agentAddress, 80); | ||
| await registry.connect(admin).addRating(secondAgent.address, 90); | ||
|
|
||
| // Remove first agent | ||
| await registry.connect(agentOwner).removeAgent(agentAddress); | ||
|
|
||
| // Verify second agent's reputation is preserved | ||
| const secondAgentData = await registry.getAgentData(secondAgent.address); | ||
| expect(secondAgentData.reputation).to.equal(90); | ||
| expect(secondAgentData.totalRatings).to.equal(1); | ||
|
|
||
| // Verify first agent's data is cleared | ||
| const firstAgentData = await registry.getAgentData(agentAddress); | ||
| expect(firstAgentData.reputation).to.equal(0); | ||
| expect(firstAgentData.totalRatings).to.equal(0); | ||
| await agentsRegistry.connect(addr2).addRating(agentAddr1.address, 75); | ||
| }); | ||
|
|
||
| it("Should handle removing agent with no proposals", async function () { | ||
| // Register agent without proposals | ||
| const [, , , , agentWithoutProposals] = await ethers.getSigners(); | ||
| await registry.connect(agentOwner).registerAgent( | ||
| agentWithoutProposals.address, | ||
| "Agent Without Proposals", | ||
| agentUri | ||
| ); | ||
|
|
||
| const removeTx = registry.connect(agentOwner).removeAgent(agentWithoutProposals.address); | ||
|
|
||
| await expect(removeTx) | ||
| .to.emit(registry, "AgentRemoved") | ||
| .withArgs(agentWithoutProposals.address, agentOwner.address); | ||
| it("Should retrieve complete agent data", async function () { | ||
| const agentData = await agentsRegistry.getAgentData(agentAddr1.address); | ||
|
|
||
| // Should not emit ProposalRemoved events since there are no proposals | ||
| await expect(removeTx).to.not.emit(registry, "ProposalRemoved"); | ||
|
|
||
| // Verify agent data is cleared | ||
| const agentData = await registry.getAgentData(agentWithoutProposals.address); | ||
| expect(agentData.agent).to.equal(ethers.ZeroAddress); | ||
| expect(agentData.name).to.equal(AGENT_NAME_1); | ||
| expect(agentData.agentUri).to.equal(AGENT_URI_1); | ||
| expect(agentData.owner).to.equal(addr1.address); | ||
| expect(agentData.agent).to.equal(agentAddr1.address); | ||
| expect(agentData.reputation).to.equal(75); | ||
| expect(agentData.totalRatings).to.equal(1); | ||
| }); | ||
|
|
||
| it("Should handle removing agent after some proposals were already removed", async function () { | ||
| // Add multiple proposals | ||
| await registry.connect(agentOwner).addProposal( | ||
| agentAddress, | ||
| "RemoveService", | ||
| ethers.parseEther("0.02"), | ||
| ethers.ZeroAddress | ||
| ); | ||
|
|
||
| // Remove one proposal manually | ||
| await registry.connect(agentOwner).removeProposal(agentAddress, 1); | ||
|
|
||
| // Remove agent (should only remove remaining active proposal) | ||
| const removeTx = registry.connect(agentOwner).removeAgent(agentAddress); | ||
|
|
||
| await expect(removeTx) | ||
| .to.emit(registry, "AgentRemoved") | ||
| .withArgs(agentAddress, agentOwner.address) | ||
| .to.emit(registry, "ProposalRemoved") | ||
| .withArgs(agentAddress, 2); | ||
| it("Should retrieve reputation separately", async function () { | ||
| const reputation = await agentsRegistry.getReputation(agentAddr1.address); | ||
| expect(reputation).to.equal(75); | ||
| }); | ||
|
|
||
| // Verify both proposals are now removed (one was manually removed, one removed by remove) | ||
| const proposal1 = await registry.getProposal(1); | ||
| const proposal2 = await registry.getProposal(2); | ||
| it("Should return zero values for non-existent agent", async function () { | ||
| const agentData = await agentsRegistry.getAgentData(agentAddr2.address); | ||
|
|
||
| expect(proposal1.issuer).to.equal(ethers.ZeroAddress); | ||
| expect(proposal1.isActive).to.equal(false); | ||
| expect(proposal2.issuer).to.equal(ethers.ZeroAddress); | ||
| expect(proposal2.isActive).to.equal(false); | ||
| expect(agentData.name).to.equal(""); | ||
| expect(agentData.agentUri).to.equal(""); | ||
| expect(agentData.owner).to.equal(ZERO_ADDRESS); | ||
| expect(agentData.agent).to.equal(ZERO_ADDRESS); | ||
| expect(agentData.reputation).to.equal(0); | ||
| expect(agentData.totalRatings).to.equal(0); | ||
| }); | ||
| }); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Add test coverage for agent migration functionality
The test suite is missing coverage for the migrateAgent function, which is a critical V1 to V2 migration path.
Add a new test section:
describe("9. Agent Migration from V1", function () {
let mockV1Registry;
beforeEach(async function () {
// Deploy proper mock V1 registry
const MockAgentRegistryV1 = await ethers.getContractFactory("MockAgentRegistryV1");
mockV1Registry = await MockAgentRegistryV1.deploy();
// Re-deploy AgentsRegistry with mock V1
agentsRegistry = await upgrades.deployProxy(
AgentsRegistry,
[await mockV1Registry.getAddress(), await serviceRegistry.getAddress()],
{ initializer: "initialize", kind: "uups" }
);
});
it("Should migrate agent from V1 registry", async function () {
// Setup V1 agent data
const v1AgentData = {
name: "V1 Agent",
agentUri: "ipfs://v1agent",
owner: addr1.address,
agent: agentAddr1.address,
reputation: 75,
totalRatings: 3,
// ... other V1 fields
};
await mockV1Registry.setAgentData(agentAddr1.address, v1AgentData);
// Migrate agent
const tx = await agentsRegistry.connect(addr1).migrateAgent(agentAddr1.address);
await expect(tx)
.to.emit(agentsRegistry, "AgentRegistered")
.withArgs(agentAddr1.address, addr1.address, "V1 Agent", "ipfs://v1agent");
// Verify migrated data
const agentData = await agentsRegistry.getAgentData(agentAddr1.address);
expect(agentData.reputation).to.equal(75);
});
it("Should prevent double migration", async function () {
// ... test preventing migration of already registered agent
});
it("Should allow owner or contract owner to migrate", async function () {
// ... test authorization logic
});
});| it("Should fail to remove non-existent agent", async function () { | ||
| await expect( | ||
| registry.connect(unregisteredAgent).setAgentData( | ||
| unregisteredAgent.address, | ||
| newAgentName, | ||
| newAgentUri | ||
| ) | ||
| agentsRegistry.connect(addr1).removeAgent(agentAddr2.address) | ||
| ).to.be.revertedWith("Not the owner of the agent"); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Test expects wrong error for removing non-existent agent
Similar to the setAgentData issue, this should expect "Agent not registered" after the contract fix.
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");
+ ).to.be.revertedWith("Agent not registered");
});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| it("Should fail to remove non-existent agent", async function () { | |
| await expect( | |
| registry.connect(unregisteredAgent).setAgentData( | |
| unregisteredAgent.address, | |
| newAgentName, | |
| newAgentUri | |
| ) | |
| agentsRegistry.connect(addr1).removeAgent(agentAddr2.address) | |
| ).to.be.revertedWith("Not the owner of the agent"); | |
| }); | |
| it("Should fail to remove non-existent agent", async function () { | |
| await expect( | |
| agentsRegistry.connect(addr1).removeAgent(agentAddr2.address) | |
| ).to.be.revertedWith("Agent not registered"); | |
| }); |
🤖 Prompt for AI Agents
In packages/contracts/test/AgentRegistry.test.js around lines 303 to 307, the
test currently expects the revert message "Not the owner of the agent" when
attempting to remove a non-existent agent; update the assertion to expect "Agent
not registered" instead so the test matches the corrected contract behavior
(i.e., change the .to.be.revertedWith(...) argument to "Agent not registered").
| it("Should allow re-registration after removal", async function () { | ||
| await agentsRegistry.connect(addr1).removeAgent(agentAddr1.address); | ||
|
|
||
| const updateTx = registry.connect(agentOwner).setAgentData( | ||
| agentAddress, | ||
| emptyName, | ||
| emptyUri | ||
| // Should be able to register the same agent address again | ||
| await agentsRegistry.connect(addr2).registerAgent( | ||
| agentAddr1.address, | ||
| AGENT_NAME_2, | ||
| AGENT_URI_2 | ||
| ); | ||
|
|
||
| await expect(updateTx) | ||
| .to.emit(registry, "AgentDataUpdated") | ||
| .withArgs(agentAddress, emptyName, emptyUri); | ||
|
|
||
| const agentData = await registry.getAgentData(agentAddress); | ||
| expect(agentData.name).to.equal(emptyName); | ||
| expect(agentData.agentUri).to.equal(emptyUri); | ||
| const agentData = await agentsRegistry.getAgentData(agentAddr1.address); | ||
| expect(agentData.owner).to.equal(addr2.address); | ||
| expect(agentData.name).to.equal(AGENT_NAME_2); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Test re-registration but not rating history persistence
The test verifies re-registration after removal but doesn't check if rating history persists or is cleared. This is important behavior to test.
it("Should allow re-registration after removal", async function () {
+ // Add a rating before removal
+ await agentsRegistry.connect(addr2).addRating(agentAddr1.address, 80);
+
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);
+
+ // Check if rating history persists (current behavior) or is cleared
+ const hasRated = await agentsRegistry.hasUserRated(agentAddr1.address, addr2.address);
+ // Document expected behavior:
+ expect(hasRated).to.equal(true); // If history persists
+ // OR expect(hasRated).to.equal(false); // If history is cleared
});Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In packages/contracts/test/AgentRegistry.test.js around lines 309-322, the
"Should allow re-registration after removal" test currently re-registers an
agent but doesn't assert what happens to the agent's rating history; add steps
to create a rating history before removal (e.g., have a third account submit one
or more ratings for agentAddr1), then remove and re-register the agent as done
now, and finally assert that the rating history is cleared (expect zero ratings)
after re-registration (and keep the existing assertions that owner and name
reflect the new registration).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 8
♻️ Duplicate comments (6)
.taskmaster/tasks/tasks.json (1)
335-403: Task 25 V2 scope captured; verify repo parity before marking subtasks doneSubtasks 25.1–25.4 are marked "done", but prior reviews flagged missing SDK methods, barrel exports, error classes, CLI "services" group, and contract updates. Please verify against the PR contents.
Run from repo root:
#!/bin/bash set -euo pipefail echo "== SDK ServiceRegistryService V2 methods ==" rg -nP --type=ts -C1 'class\s+ServiceRegistryService\b' packages | sed 's/^/ /' || true rg -nP --type=ts -n '\b(createService|updateService|deleteService|getService|listServices|assignAgent|unassignAgent|getAssignedAgents|getServicesByAgent|transferOwnership|getServiceOwner)\s*\(' packages | sed 's/^/ /' || true echo "== Service schemas/types re-exported ==" rg -nP --type=ts -n 'export .* from .*/schemas/service\.schemas' packages/sdk/src/index.ts packages/sdk/src/types.ts | sed 's/^/ /' || true echo "== New error classes present and referenced ==" rg -nP --type=ts -n 'class\s+Service(?:NotFound|Ownership|Status|Validation|AgentAssignment)Error\b' packages/sdk | sed 's/^/ /' || true rg -nP --type=ts -n 'Service(?:NotFound|Ownership|Status|Validation|AgentAssignment)Error' packages | sed 's/^/ /' || true echo "== CLI 'ensemble services' commands ==" rg -nP -n '\bensemble\s+services\b' packages | sed 's/^/ /' || true rg -nP -n '\bservices\s+(create|list|get|update|delete|assign|unassign|transfer)\b' packages | sed 's/^/ /' || true echo "== Contracts/ABIs expose V2 events/functions ==" rg -nP -n 'ServiceCreated|ServiceUpdated|ServiceDeleted|AgentAssigned|AgentUnassigned|OwnershipTransferred' -g 'packages/**/{artifacts,abi,contracts}/**/*' | sed 's/^/ /' || true rg -nP -n '\b(createService|updateService|transferOwnership|assignAgent|revokeAgent|getService|listServices)\b' -g 'packages/**/contracts/**/*.sol' | sed 's/^/ /' || true echo "== IPFS integration in SDK ==" rg -nP --type=ts -n 'ipfs-http-client|create.*IPFS|pin' packages/sdk | sed 's/^/ /' || trueOptional: add acceptance criteria to 25.5 for unambiguous DoD.
{ "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.", + "acceptanceCriteria": [ + "SDK: listServices supports filters {category,status,owner,assignedAgent,tags,dateRange} and cursor pagination (nextCursor, prevCursor).", + "CLI: ensemble services {create|list|get|update|delete|assign|unassign|transfer} wired to SDK with Zod validation and --format output options.", + "Docs: service-record.yaml schema documented; examples added.", + "E2E: Passing tests for create→assign→transfer→update→list→delete workflow on testnet." + ], "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." }packages/sdk/src/services/ServiceRegistryService.ts (5)
111-120: Buffer isn’t available in browsers; add a cross‑platform base64 fallback.- serviceUri = `data:application/json;base64,${Buffer.from(JSON.stringify(metadataWithTimestamps)).toString('base64')}`; + const json = JSON.stringify(metadataWithTimestamps); + const toBase64 = () => { + if (typeof Buffer !== 'undefined') return Buffer.from(json, 'utf-8').toString('base64'); + const bytes = new TextEncoder().encode(json); + let bin = ''; + for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]); + return btoa(bin); + }; + serviceUri = `data:application/json;base64,${toBase64()}`;Apply the same change to the fallback in updateService.
236-251: Repeat of base64 fallback note for updateService.- serviceUri = `data:application/json;base64,${Buffer.from(JSON.stringify(metadata)).toString('base64')}`; + const json = JSON.stringify(metadata); + const toBase64 = () => { + if (typeof Buffer !== 'undefined') return Buffer.from(json, 'utf-8').toString('base64'); + const bytes = new TextEncoder().encode(json); + let bin = ''; + for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]); + return btoa(bin); + }; + serviceUri = `data:application/json;base64,${toBase64()}`;
864-871: Use Interface.getEventTopic for stable topic lookup; guard logs.- const serviceRegisteredTopic = this.serviceRegistry.interface.getEvent('ServiceRegistered').topicHash; - const event = receipt.logs.find((log: any) => log.topics[0] === serviceRegisteredTopic); + const topic = this.serviceRegistry.interface.getEventTopic('ServiceRegistered'); + const event = receipt.logs.find((log: any) => log.topics?.[0] === topic);
913-929: Align status transition rules with delete policy and include serviceId in errors.
- Disallow published → deleted via transition helper (keep delete via dedicated deleteService flow if desired).
- Include serviceId in ServiceStatusError.
- Update caller to pass serviceId.
- private validateStatusTransition(currentStatus: ServiceStatus, newStatus: ServiceStatus): void { + private validateStatusTransition(serviceId: string, currentStatus: ServiceStatus, newStatus: ServiceStatus): void { const validTransitions: Record<ServiceStatus, ServiceStatus[]> = { 'draft': ['published', 'deleted'], - 'published': ['archived', 'deleted'], + 'published': ['archived'], 'archived': ['published', 'deleted'], 'deleted': [] }; const allowedTransitions = validTransitions[currentStatus] || []; if (!allowedTransitions.includes(newStatus)) { - throw new ServiceStatusError( - "unknown", - currentStatus, - `Cannot transition from ${currentStatus} to ${newStatus}. Allowed: ${allowedTransitions.join(', ')}` - ); + throw new ServiceStatusError(serviceId, currentStatus, allowedTransitions.join(' or ') || 'n/a'); } }- this.validateStatusTransition(service.status, status); + this.validateStatusTransition(serviceId, service.status, status);Additionally, deleteService already blocks deletion from 'published'; keep that check.
Also applies to: 429-444, 277-301
315-332: Fail fast with a clear NotImplemented for read-dependent flows until getServiceById exists.
- Replace the generic throw in getServiceById with a clear NotImplemented message.
- Consider guarding update/delete/transfer/status/assign/unassign/getServiceOwner/getAssignedAgents/getServicesByAgent to throw the same until read APIs are wired.
- // 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"); + throw new Error("NotImplemented: getServiceById requires ServiceRegistry V2 read wiring");I can add a feature flag (e.g., this.readAPISupported) and gate dependent methods—say the word and I’ll push a patch.
Also applies to: 339-359, 372-405, 412-415, 429-459, 499-542, 599-625, 632-643
🧹 Nitpick comments (10)
.taskmaster/tasks/tasks.json (2)
404-417: Task 26 architecture plan is solid; add migration artifacts to reduce churnInclude explicit deliverables for migration (MIGRATING.md, deprecation map) and a deprecation timeline to safeguard SDK consumers.
Apply:
{ "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", + "deliverables": [ + "MIGRATING.md with old->new API mapping and examples", + "DEPRECATIONS.md with timelines and adapter coverage", + "CHANGELOG entries noting refactor and any soft breaks" + ], "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": [] }
421-421: Timestamp update is fine; ensure CI owns this fieldConsider automating master.metadata.updated in CI to avoid manual drift.
packages/sdk/src/services/ServiceRegistryService.ts (4)
649-692: Prefer reusing uploadMetadata and pinContent in register/update to DRY and centralize retries.You duplicate ipfsSDK.upload.json logic. Refactor registerService and updateService to call uploadMetadata(metadata) to reuse retries/pinning.
576-581: Map ServiceStatus to on-chain enum via a helper for readability and safety.Inline numbers (0, 2) are brittle. Suggest:
const toOnChainStatus = (s: ServiceStatus) => s === 'draft' ? 0 : s === 'published' ? 1 : s === 'archived' ? 2 : 3;Then use
toOnChainStatus(newStatus).
498-505: Validate inputs and short-circuit early with explicit messages.For assignAgentToService, add a quick equality check before any async calls to save an RPC:
+ if (agentAddress.toLowerCase() === (await this.signer!.getAddress()).toLowerCase()) { + console.warn("Assigning the service owner as agent; is this intended?"); + }Also applies to: 519-525
348-354: listServices placeholder: return empty array but logs warn.Consider returning an empty array without console.warn in production builds; guard with NODE_ENV.
packages/contracts/contracts/ServiceRegistryUpgradeable.sol (3)
246-266: Consider bumping version on agent assignment and ownership transfer for cache invalidation.
- increment
service.version++in assignAgentToService and transferServiceOwnership to reflect meaningful state change.
322-325: Clarify “total” semantics or rename.getTotalServiceCount currently tracks cumulative creations (not active). Rename to getTotalCreatedServices or document clearly.
352-361: Array removals are O(n). Consider index maps if agent/owner lists grow.Introduce mapping(serviceId => index) to enable O(1) removals.
Also applies to: 368-377
packages/contracts/test/ServiceRegistryV2.Phase2.test.js (1)
1-31: Optional: assert initial state after deploy.Add quick checks for nextServiceId == 0 and totalServices == 0 to catch init regressions.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (4)
.taskmaster/tasks/tasks.json(1 hunks)packages/contracts/contracts/ServiceRegistryUpgradeable.sol(2 hunks)packages/contracts/test/ServiceRegistryV2.Phase2.test.js(1 hunks)packages/sdk/src/services/ServiceRegistryService.ts(2 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
packages/contracts/contracts/**/*.sol
📄 CodeRabbit inference engine (packages/contracts/.cursor/rules/project-intelligence.mdc)
packages/contracts/contracts/**/*.sol: All public functions in Solidity contracts must have complete NatSpec documentation comments
Custom errors with descriptive names should be used for error handling in Solidity contracts instead of string messages
All state changes in Solidity contracts must emit appropriate events
Follow OpenZeppelin security patterns in all Solidity contracts
Files:
packages/contracts/contracts/ServiceRegistryUpgradeable.sol
packages/contracts/**/*.sol
📄 CodeRabbit inference engine (packages/contracts/.cursor/rules/role.mdc)
packages/contracts/**/*.sol: Use explicit function visibility modifiers and appropriate natspec comments.
Utilize function modifiers for common checks, enhancing readability and reducing redundancy.
Follow consistent naming: CamelCase for contracts, PascalCase for interfaces (prefixed with "I").
Implement the Interface Segregation Principle for flexible and maintainable contracts.
Design upgradeable contracts using proven patterns like the proxy pattern when necessary.
Implement comprehensive events for all significant state changes.
Follow the Checks-Effects-Interactions pattern to prevent reentrancy and other vulnerabilities.
Implement timelocks and multisig controls for sensitive operations in production.
Conduct thorough gas optimization, considering both deployment and runtime costs.
Use OpenZeppelin's AccessControl for fine-grained permissions.
Use Solidity 0.8.0+ for built-in overflow/underflow protection.
Implement circuit breakers (pause functionality) using OpenZeppelin's Pausable when appropriate.
Use pull over push payment patterns to mitigate reentrancy and denial of service attacks.
Implement rate limiting for sensitive functions to prevent abuse.
Use OpenZeppelin's SafeERC20 for interacting with ERC20 tokens.
Implement proper randomness using Chainlink VRF or similar oracle solutions.
Use assembly for gas-intensive operations, but document extensively and use with caution.
Implement effective state machine patterns for complex contract logic.
Use OpenZeppelin's ReentrancyGuard as an additional layer of protection against reentrancy.
Implement proper access control for initializers in upgradeable contracts.
Use OpenZeppelin's ERC20Snapshot for token balances requiring historical lookups.
Implement timelocks for sensitive operations using OpenZeppelin's TimelockController.
Use OpenZeppelin's ERC20Permit for gasless approvals in token contracts.
Implement proper slippage protection for DEX-like functionalities.
Use OpenZeppelin's ERC20Votes for governa...
Files:
packages/contracts/contracts/ServiceRegistryUpgradeable.sol
🧠 Learnings (3)
📚 Learning: 2025-08-01T07:27:26.601Z
Learnt from: CR
PR: ensemble-codes/ensemble-framework#0
File: packages/contracts/.cursor/rules/project-intelligence.mdc:0-0
Timestamp: 2025-08-01T07:27:26.601Z
Learning: Applies to packages/contracts/test/*Registry*.sol : All registry contracts must have extensive test coverage, including edge cases and integration scenarios
Applied to files:
packages/contracts/test/ServiceRegistryV2.Phase2.test.js
📚 Learning: 2025-08-01T07:27:26.601Z
Learnt from: CR
PR: ensemble-codes/ensemble-framework#0
File: packages/contracts/.cursor/rules/project-intelligence.mdc:0-0
Timestamp: 2025-08-01T07:27:26.601Z
Learning: Applies to packages/contracts/test/**/*{Integration,EnsembleCredits,Registry}*.sol : Integration testing must verify interaction between EnsembleCredits and registry contracts
Applied to files:
packages/contracts/test/ServiceRegistryV2.Phase2.test.js
📚 Learning: 2025-08-01T07:27:26.601Z
Learnt from: CR
PR: ensemble-codes/ensemble-framework#0
File: packages/contracts/.cursor/rules/project-intelligence.mdc:0-0
Timestamp: 2025-08-01T07:27:26.601Z
Learning: Applies to packages/contracts/test/EnsembleCredits*.sol : EnsembleCredits contract must have a comprehensive test suite covering edge cases, security scenarios, gas optimization, and integration with the registry system before deployment
Applied to files:
packages/contracts/test/ServiceRegistryV2.Phase2.test.js
🧬 Code graph analysis (1)
packages/sdk/src/services/ServiceRegistryService.ts (6)
packages/sdk/src/index.ts (6)
ServiceRegistryService(50-50)validateRegisterServiceParams(28-28)parseRegisterServiceParams(34-34)validateServiceRecord(26-26)validateUpdateServiceParams(29-29)parseUpdateServiceParams(35-35)packages/contracts/test/ServiceRegistry.test.js (9)
ServiceRegistry(5-5)tx(51-51)tx(73-73)tx(153-153)tx(273-273)service(59-59)service(79-79)service(170-170)services(203-203)packages/sdk/src/schemas/service.schemas.ts (9)
RegisterServiceParams(207-207)ServiceRecord(206-206)validateRegisterServiceParams(237-239)parseRegisterServiceParams(292-306)ServiceStatus(200-200)validateServiceRecord(222-224)UpdateServiceParams(208-208)validateUpdateServiceParams(246-248)parseUpdateServiceParams(313-327)packages/sdk/src/types.ts (4)
RegisterServiceParams(91-91)ServiceRecord(87-87)ServiceStatus(95-95)UpdateServiceParams(92-92)packages/sdk/src/errors.ts (6)
ServiceValidationError(58-63)ServiceAlreadyRegisteredError(22-27)ServiceStatusError(51-56)ServiceNotFoundError(37-42)ServiceAgentAssignmentError(65-70)ServiceOwnershipError(44-49)packages/sdk/src/ensemble.ts (1)
services(47-49)
🪛 GitHub Actions: PR Validation
packages/sdk/src/services/ServiceRegistryService.ts
[error] 723-723: tsc --noEmit failed with TS2322: Type 'unknown' is not assignable to type 'IPFSMetadata'.
🔇 Additional comments (8)
.taskmaster/tasks/tasks.json (1)
334-334: JSON structure continuation is correctTrailing comma and bracket placement look valid for introducing new tasks.
packages/sdk/src/services/ServiceRegistryService.ts (1)
723-723: CI blocker cleared: TS2322 resolved. Verified withpnpm -w -s tsc --noEmit(no errors).packages/contracts/contracts/ServiceRegistryUpgradeable.sol (1)
267-295: LGTM: fixed old/new status emission on unassign.Capturing oldStatus before mutation prevents duplicate values in ServiceStatusChanged.
packages/contracts/test/ServiceRegistryV2.Phase2.test.js (5)
296-312: Nice coverage on update validation and permissions.Also applies to: 317-324
238-248: Good negative tests around invalid statuses and no-agent case.Also applies to: 250-258
33-52: Status transition tests align with contract behavior.Also applies to: 63-76, 78-92, 93-105, 106-115
190-194: Explicit BigInt usage is correct here.Using ethers.getBigInt for array comparison is appropriate.
352-383: End-to-end workflows look solid and readable.Also applies to: 385-405, 407-429
| // ============================================================================ | ||
| // 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"); | ||
| _; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Adopt custom errors and replace revert strings.
Project guidelines require custom errors for gas savings and clarity.
Example (apply throughout):
+error NotServiceOwner();
+error ServiceDoesNotExist();
+error ServiceUriRequired();
+error InvalidAgent();
+error InvalidStatusTransition();
+error CannotUpdateDeleted();
+error NoAgentAssigned();
+error CannotDeletePublished();
- modifier onlyServiceOwner(uint256 serviceId) {
- require(services[serviceId].owner == msg.sender, "Not service owner");
+ modifier onlyServiceOwner(uint256 serviceId) {
+ if (services[serviceId].owner != msg.sender) revert NotServiceOwner();
_;
}
- modifier serviceExists(uint256 serviceId) {
- require(serviceId > 0 && serviceId <= nextServiceId, "Service does not exist");
+ modifier serviceExists(uint256 serviceId) {
+ if (!(serviceId > 0 && serviceId <= nextServiceId)) revert ServiceDoesNotExist();
_;
}
- require(bytes(serviceUri).length > 0, "Service URI required");
+ if (bytes(serviceUri).length == 0) revert ServiceUriRequired();…and similarly for other require/revert sites.
Also applies to: 118-126, 134-149, 155-179, 186-199, 209-226, 246-266, 272-295, 300-317, 322-325
🤖 Prompt for AI Agents
In packages/contracts/contracts/ServiceRegistryUpgradeable.sol around lines 63
to 75 (and also for the other indicated ranges 118-126, 134-149, 155-179,
186-199, 209-226, 246-266, 272-295, 300-317, 322-325), replace require(...)
revert strings with custom error types: add error declarations at the top of the
contract (e.g., error NotServiceOwner(address caller, uint256 serviceId); error
ServiceNotFound(uint256 serviceId); and similar domain-specific errors for each
distinct message), then update each require to revert using the corresponding
custom error (e.g., if owner mismatch revert NotServiceOwner(msg.sender,
serviceId); if invalid id revert ServiceNotFound(serviceId);), ensure any errors
that need extra context include relevant parameters, update all listed line
ranges consistently, and run the compiler to fix any type/signature usages
resulting from the replacements.
| 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); | ||
| } | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Align deletion policy with SDK (block delete from PUBLISHED).
SDK’s deleteService forbids deletion from 'published'. Enforce on-chain to avoid surprising divergence.
function deleteService(uint256 serviceId)
external
serviceExists(serviceId)
onlyServiceOwner(serviceId)
{
Service storage service = services[serviceId];
- require(service.status != ServiceStatus.DELETED, "Service already deleted");
+ if (service.status == ServiceStatus.DELETED) revert InvalidStatusTransition();
+ if (service.status == ServiceStatus.PUBLISHED) revert CannotDeletePublished();
ServiceStatus oldStatus = service.status;
service.status = ServiceStatus.DELETED;
emit ServiceStatusChanged(serviceId, oldStatus, ServiceStatus.DELETED);
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function deleteService(uint256 serviceId) | |
| external | |
| serviceExists(serviceId) | |
| onlyServiceOwner(serviceId) | |
| { | |
| Service storage service = services[serviceId]; | |
| require(service.status != ServiceStatus.DELETED, "Service already deleted"); | |
| ServiceStatus oldStatus = service.status; | |
| service.status = ServiceStatus.DELETED; | |
| emit ServiceStatusChanged(serviceId, oldStatus, ServiceStatus.DELETED); | |
| } | |
| function deleteService(uint256 serviceId) | |
| external | |
| serviceExists(serviceId) | |
| onlyServiceOwner(serviceId) | |
| { | |
| Service storage service = services[serviceId]; | |
| if (service.status == ServiceStatus.DELETED) revert InvalidStatusTransition(); | |
| if (service.status == ServiceStatus.PUBLISHED) revert CannotDeletePublished(); | |
| ServiceStatus oldStatus = service.status; | |
| service.status = ServiceStatus.DELETED; | |
| emit ServiceStatusChanged(serviceId, oldStatus, ServiceStatus.DELETED); | |
| } |
🤖 Prompt for AI Agents
In packages/contracts/contracts/ServiceRegistryUpgradeable.sol around lines 186
to 199, the contract currently allows deleting services in any non-DELETED state
which diverges from the SDK that forbids deletion of PUBLISHED services; add a
guard that prevents deletion when service.status == ServiceStatus.PUBLISHED
(e.g., require(service.status != ServiceStatus.PUBLISHED, "Cannot delete
published service")) before updating status, and keep/update the existing
DELETED check/message as appropriate so on-chain behavior matches the SDK.
| 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 | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix bigint comparison literal.
version is a uint256 (BigInt in ethers v6). Comparing to 4 (number) can fail; use 4n.
- expect(service.version).to.equal(4); // Initial version 1 + 3 updates
+ expect(service.version).to.equal(4n); // Initial version 1 + 3 updates📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| 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 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(4n); // Initial version 1 + 3 updates | |
| }); |
🤖 Prompt for AI Agents
In packages/contracts/test/ServiceRegistryV2.Phase2.test.js around lines 325 to
337, the assertion compares a BigInt uint256 (service.version) to a JS number
(4), which can fail with ethers v6; update the expectation to compare against a
BigInt (e.g., 4n or BigInt(4)) so the types match (change
expect(service.version).to.equal(4) to expect(service.version).to.equal(4n)).
| // Register service on blockchain with minimal data | ||
| const tx = await this.serviceRegistry.registerService( | ||
| service.name, | ||
| service.category, | ||
| service.description | ||
| serviceUri, | ||
| parsedParams.agentAddress || ethers.ZeroAddress | ||
| ); | ||
| console.log(`Transaction sent for service ${service.name}: ${tx.hash}`); | ||
|
|
||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Connect the contract to the signer for all write calls.
registerService, updateService, and unassignAgentFromService send transactions but don’t .connect(this.signer). This will fail when the instance was constructed with a provider-only contract.
Apply:
- const tx = await this.serviceRegistry.registerService(
+ const tx = await this.serviceRegistry.connect(this.signer!).registerService(
serviceUri,
parsedParams.agentAddress || ethers.ZeroAddress
);- const tx = await this.serviceRegistry.updateService(
+ const tx = await this.serviceRegistry.connect(this.signer!).updateService(
serviceId,
serviceUri
);- const tx = await this.serviceRegistry.unassignAgentFromService(
+ const tx = await this.serviceRegistry.connect(this.signer!).unassignAgentFromService(
BigInt(serviceId),
newStatus === 'draft' ? 0 : 2 // DRAFT = 0, ARCHIVED = 2
);Also applies to: 253-257, 576-581
🤖 Prompt for AI Agents
In packages/sdk/src/services/ServiceRegistryService.ts around lines 121-126,
253-257 and 576-581 the contract write calls (registerService, updateService,
unassignAgentFromService) are invoked directly on a provider-bound contract and
must be connected to the signer; update each call to use
this.serviceRegistry.connect(this.signer).registerService(...),
this.serviceRegistry.connect(this.signer).updateService(...), and
this.serviceRegistry.connect(this.signer).unassignAgentFromService(...),
respectively, and guard with a clear error if this.signer is undefined so writes
fail fast.
| // 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')}`; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
updateService returns a stale serviceUri.
You compute a new serviceUri but never put it on the returned object.
- // Update service on blockchain with new IPFS URI
- const tx = await this.serviceRegistry.updateService(
+ // Update service on blockchain with new IPFS URI
+ const tx = await this.serviceRegistry.connect(this.signer!).updateService(
serviceId,
serviceUri
);
const receipt = await tx.wait();
console.log(`Service updated successfully: ${serviceId} (tx: ${receipt?.hash})`);
- return updatedService;
+ const finalService: ServiceRecord = { ...updatedService, serviceUri };
+ return finalService;Also applies to: 262-263
🤖 Prompt for AI Agents
In packages/sdk/src/services/ServiceRegistryService.ts around lines 217 to 251
(and also apply same fix at 262-263), you compute a new serviceUri when updating
metadata but never assign it back to the updatedService returned to callers; set
updatedService.serviceUri = serviceUri after computing it in both the IPFS and
fallback branches (and ensure the later return path at lines 262-263 uses that
updatedService with the new serviceUri) so callers receive the fresh URI.
| async downloadMetadata(ipfsUri: string): Promise<IPFSMetadata> { | ||
| 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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Fix TS build: response.json() is ‘unknown’; also drop unnecessary IPFS SDK gate for reads.
This causes TS2322 in CI and blocks read-only usage.
async downloadMetadata(ipfsUri: string): Promise<IPFSMetadata> {
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}`);
+ const controller = new AbortController();
+ const timeout = setTimeout(() => controller.abort(), 15_000);
+ const response = await fetch(`https://gateway.pinata.cloud/ipfs/${ipfsHash}`, { signal: controller.signal }).finally(() => clearTimeout(timeout));
if (!response.ok) {
throw new IPFSError(`HTTP ${response.status}: ${response.statusText}`);
}
- const metadata = await response.json();
+ const metadata = (await response.json()) as IPFSMetadata;
console.log(`Metadata downloaded from IPFS: ${ipfsUri}`);
return metadata;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| async downloadMetadata(ipfsUri: string): Promise<IPFSMetadata> { | |
| 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; | |
| async downloadMetadata(ipfsUri: string): Promise<IPFSMetadata> { | |
| if (!ipfsUri.startsWith('ipfs://')) { | |
| throw new IPFSError('Invalid IPFS URI format'); | |
| } | |
| const ipfsHash = ipfsUri.replace('ipfs://', ''); | |
| try { | |
| console.log(`Downloading metadata from IPFS: ${ipfsUri}`); | |
| // Use Pinata gateway or public gateway with a 15s timeout | |
| const controller = new AbortController(); | |
| const timeout = setTimeout(() => controller.abort(), 15_000); | |
| const response = await fetch( | |
| `https://gateway.pinata.cloud/ipfs/${ipfsHash}`, | |
| { signal: controller.signal } | |
| ).finally(() => clearTimeout(timeout)); | |
| if (!response.ok) { | |
| throw new IPFSError(`HTTP ${response.status}: ${response.statusText}`); | |
| } | |
| const metadata = (await response.json()) as IPFSMetadata; | |
| console.log(`Metadata downloaded from IPFS: ${ipfsUri}`); | |
| return metadata; | |
| } catch (err) { | |
| // existing error handling... | |
| } | |
| } |
🧰 Tools
🪛 GitHub Actions: PR Validation
[error] 723-723: tsc --noEmit failed with TS2322: Type 'unknown' is not assignable to type 'IPFSMetadata'.
🤖 Prompt for AI Agents
In packages/sdk/src/services/ServiceRegistryService.ts around lines 699 to 723,
TypeScript fails because response.json() is typed as unknown and there's an
unnecessary guard that throws if this.ipfsSDK is not set; remove the IPFS SDK
presence check and its throw (allow read-only IPFS fetches), and explicitly type
the parsed JSON as IPFSMetadata (e.g., assign await response.json() to a
variable cast to IPFSMetadata or use a generic) so the function returns the
correct IPFSMetadata type.
| async createServiceWithMetadata(serviceData: RegisterServiceParams): Promise<ServiceRecord> { | ||
| 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) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Avoid double IPFS uploads in createServiceWithMetadata.
You upload metadata, then call registerService which uploads again. This doubles cost/latency.
Option A (preferred): change registerService to accept a prebuilt serviceUri.
Option B: skip the explicit upload here and rely on registerService to upload metadata.
- // 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);
+ // Delegate upload to registerService to avoid double uploads
+ return this.registerService(serviceData);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| async createServiceWithMetadata(serviceData: RegisterServiceParams): Promise<ServiceRecord> { | |
| 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) { | |
| async createServiceWithMetadata(serviceData: RegisterServiceParams): Promise<ServiceRecord> { | |
| 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() | |
| }; | |
| // Delegate upload to registerService to avoid double uploads | |
| return this.registerService(serviceData); | |
| } catch (error: any) { | |
| // existing error handling... |
| async updateServiceMetadata(serviceId: string, metadata: IPFSMetadata): Promise<ServiceRecord> { | ||
| 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) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
updateServiceMetadata uploads but discards ipfsUri; results in a second upload inside updateService.
Consolidate to a single upload.
- // Upload new metadata to IPFS
- await this.uploadMetadata({
- ...metadata,
- updatedAt: new Date().toISOString()
- });
-
- // Update service with new metadata
+ // Update service with metadata; updateService will handle the upload
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);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| async updateServiceMetadata(serviceId: string, metadata: IPFSMetadata): Promise<ServiceRecord> { | |
| 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) { | |
| async updateServiceMetadata(serviceId: string, metadata: IPFSMetadata): Promise<ServiceRecord> { | |
| this.requireSigner(); | |
| try { | |
| // Get current service to verify ownership | |
| const currentService = await this.getServiceById(serviceId); | |
| await this.verifyServiceOwnership(currentService, await this.signer!.getAddress()); | |
| // Update service with metadata; updateService will handle the upload | |
| 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) { | |
| // … |
🤖 Prompt for AI Agents
In packages/sdk/src/services/ServiceRegistryService.ts around lines 817 to 847,
the method uploads metadata to IPFS but ignores the returned ipfsUri, causing
updateService to re-upload; capture the return value from uploadMetadata (e.g.
const { ipfsUri } = await this.uploadMetadata(...)) and include that ipfsUri in
the updateParams.metadata object (add the ipfsUri field or the expected key
updateService reads) so updateService receives the already-uploaded URI and does
not perform a second upload; ensure uploadMetadata's return type is used and
propagated to updateService.
Summary by CodeRabbit
New Features
Changes
Removals
Documentation