├── .gitignore ├── packages ├── python-sdk │ ├── src │ │ ├── __init__.py │ │ ├── services │ │ │ ├── contract_service.py │ │ │ ├── task_service.py │ │ │ ├── agent_service.py │ │ │ └── proposal_service.py │ │ ├── types.py │ │ ├── abi │ │ │ ├── TaskRegistry.abi.json │ │ │ └── AgentsRegistry.abi.json │ │ └── ensemble.py │ ├── tests │ │ ├── __init__.py │ │ └── conftest.py │ ├── ai_agents_sdk.egg-info │ │ ├── dependency_links.txt │ │ ├── top_level.txt │ │ ├── requires.txt │ │ ├── PKG-INFO │ │ └── SOURCES.txt │ └── setup.py ├── contracts │ ├── .npmrc │ ├── .DS_Store │ ├── ignition │ │ ├── params │ │ │ ├── ensemble-credits.json │ │ │ ├── local.json │ │ │ └── baseSepolia.json │ │ ├── deployments │ │ │ ├── chain-84532 │ │ │ │ ├── artifacts │ │ │ │ │ ├── EnsembleCreditsModule#EnsembleCredits.dbg.json │ │ │ │ │ ├── TaskRegistryUpgradeableModule#TaskRegistryImpl.dbg.json │ │ │ │ │ ├── TaskRegistryUpgradeableModule#TaskRegistryProxied.dbg.json │ │ │ │ │ ├── TaskRegistryUpgradeableModule#TaskRegistryProxy.dbg.json │ │ │ │ │ ├── AgentsRegistryUpgradeableModule#AgentsRegistryImpl.dbg.json │ │ │ │ │ ├── AgentsRegistryUpgradeableModule#AgentsRegistryProxied.dbg.json │ │ │ │ │ ├── AgentsRegistryUpgradeableModule#AgentsRegistryProxy.dbg.json │ │ │ │ │ ├── ServiceRegistryUpgradeableModule#ServiceRegistryImpl.dbg.json │ │ │ │ │ ├── ServiceRegistryUpgradeableModule#ServiceRegistryProxy.dbg.json │ │ │ │ │ ├── ServiceRegistryUpgradeableModule#ServiceRegistryProxied.dbg.json │ │ │ │ │ ├── TaskRegistryUpgradeableModule#TaskRegistryProxy.json │ │ │ │ │ ├── AgentsRegistryUpgradeableModule#AgentsRegistryProxy.json │ │ │ │ │ └── ServiceRegistryUpgradeableModule#ServiceRegistryProxy.json │ │ │ │ └── deployed_addresses.json │ │ │ └── chain-50312 │ │ │ │ └── artifacts │ │ │ │ └── AgentsRegistryUpgradeableModule#AgentsRegistryImpl.dbg.json │ │ └── modules │ │ │ ├── EnsembleCredits.ts │ │ │ ├── ServiceRegistryUpgradeable.ts │ │ │ ├── TaskRegistryUpgradeable.ts │ │ │ ├── AgentsRegistryUpgradeable.ts │ │ │ ├── DeployAll.ts │ │ │ └── UpgradeRegistries.ts │ ├── start.sh │ ├── scripts │ │ ├── verify.sh │ │ ├── create-task.ts │ │ ├── deploy.js │ │ └── deploy.ts │ ├── tsconfig.json │ ├── .gitignore │ ├── contracts │ │ ├── interfaces │ │ │ ├── IProposalStruct.sol │ │ │ ├── IAgent.sol │ │ │ ├── ITask.sol │ │ │ └── IAgentRegistryV1.sol │ │ ├── Proxies.sol │ │ ├── mocks │ │ │ └── MockERC20.sol │ │ ├── lib │ │ │ └── TransferHelper.sol │ │ └── ServiceRegistryUpgradeable.sol │ ├── memory-bank │ │ ├── projectbrief.md │ │ └── productContext.md │ ├── package.json │ ├── test │ │ └── ServiceRegistry.test.js │ └── hardhat.config.ts ├── .DS_Store ├── sdk │ ├── docs │ │ ├── assets │ │ │ ├── hierarchy.js │ │ │ ├── navigation.js │ │ │ └── highlight.css │ │ └── .nojekyll │ ├── .graphclientrc.yml │ ├── jest.config.js │ ├── src │ │ ├── utils │ │ │ └── parsePrompt.ts │ │ ├── services │ │ │ ├── ContractService.ts │ │ │ └── ServiceRegistryService.ts │ │ ├── index.ts │ │ └── errors.ts │ ├── scripts │ │ ├── create-task.ts │ │ ├── register-services.ts │ │ ├── register-agents.ts │ │ ├── create-proposals.ts │ │ ├── data │ │ │ ├── agentsList.ts │ │ │ ├── servicesList.ts │ │ │ └── proposalsList.ts │ │ └── utils │ │ │ └── setupSdk.ts │ ├── __mocks__ │ │ └── graphql-request.js │ ├── tsconfig.json │ ├── index.ts │ ├── .env.example │ ├── test │ │ └── optionalSigner.basic.test.ts │ ├── .gitignore │ └── package.json ├── subgraph │ ├── tsconfig.json │ ├── networks.json │ ├── .gitignore │ ├── src │ │ ├── utils.ts │ │ ├── constants.ts │ │ ├── service-registry.ts │ │ ├── task-registry.ts │ │ ├── agents-registry.ts │ │ └── fds.ts │ ├── schema.graphql │ ├── package.json │ ├── docker-compose.yml │ ├── tests │ │ ├── agents-registry.test.ts │ │ ├── task-registry.test.ts │ │ ├── agents-registry-utils.ts │ │ └── task-registry-utils.ts │ └── subgraph.yaml ├── mcp-server │ ├── .env.example │ ├── src │ │ ├── subgraph │ │ │ └── client.ts │ │ └── config.ts │ ├── tsconfig.json │ ├── jest.config.ts │ ├── .gitignore │ ├── package.json │ ├── README.md │ └── tests │ │ └── agent-matcher.test.ts └── cli │ ├── jest.config.js │ ├── tsconfig.json │ ├── src │ ├── utils │ │ ├── wallet.ts │ │ ├── sdk.ts │ │ └── file-operations.ts │ ├── types │ │ ├── config.ts │ │ └── wallet.ts │ ├── commands │ │ ├── agents │ │ │ ├── index.ts │ │ │ └── list.ts │ │ └── config.ts │ ├── bin │ │ └── ensemble.ts │ └── config │ │ └── manager.ts │ ├── CHANGELOG.md │ ├── .gitignore │ └── package.json ├── pnpm-workspace.yaml ├── .hh ├── .DS_Store ├── generated-icon.png ├── settings.json ├── assets └── ensemble-stack-image.png ├── .taskmaster ├── state.json ├── tasks │ ├── task_003.txt │ ├── task_001.txt │ ├── task_006.txt │ ├── task_023.txt │ ├── task_017.txt │ ├── task_016.txt │ └── task_005.txt ├── config.json └── templates │ └── example_prd.txt ├── .replit ├── .changeset ├── config.json └── README.md ├── .github └── workflows │ ├── pr-validation.yml │ ├── release.yml │ └── prerelease.yml └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .env -------------------------------------------------------------------------------- /packages/python-sdk/src/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /packages/python-sdk/tests/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/*' -------------------------------------------------------------------------------- /.hh: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | telemetry: false 3 | } 4 | -------------------------------------------------------------------------------- /packages/python-sdk/ai_agents_sdk.egg-info/dependency_links.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /packages/contracts/.npmrc: -------------------------------------------------------------------------------- 1 | hardhat-telemetry=false 2 | save-exact=true 3 | -------------------------------------------------------------------------------- /packages/python-sdk/ai_agents_sdk.egg-info/top_level.txt: -------------------------------------------------------------------------------- 1 | src 2 | tests 3 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ensemble-codes/ensemble-framework/HEAD/.DS_Store -------------------------------------------------------------------------------- /generated-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ensemble-codes/ensemble-framework/HEAD/generated-icon.png -------------------------------------------------------------------------------- /packages/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ensemble-codes/ensemble-framework/HEAD/packages/.DS_Store -------------------------------------------------------------------------------- /packages/sdk/docs/assets/hierarchy.js: -------------------------------------------------------------------------------- 1 | window.hierarchyData = "eJyrVirKzy8pVrKKjtVRKkpNy0lNLsnMzytWsqqurQUAmx4Kpg==" -------------------------------------------------------------------------------- /settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "**/dist": true, 4 | "**/dist/**": true 5 | } 6 | } -------------------------------------------------------------------------------- /packages/contracts/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ensemble-codes/ensemble-framework/HEAD/packages/contracts/.DS_Store -------------------------------------------------------------------------------- /assets/ensemble-stack-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ensemble-codes/ensemble-framework/HEAD/assets/ensemble-stack-image.png -------------------------------------------------------------------------------- /packages/subgraph/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@graphprotocol/graph-ts/types/tsconfig.base.json", 3 | "include": ["src", "tests"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/python-sdk/ai_agents_sdk.egg-info/requires.txt: -------------------------------------------------------------------------------- 1 | web3>=6.0.0 2 | eth-typing>=3.0.0 3 | python-dotenv>=1.0.0 4 | google-cloud-pubsub>=2.27.1 5 | -------------------------------------------------------------------------------- /packages/sdk/docs/.nojekyll: -------------------------------------------------------------------------------- 1 | TypeDoc added this file to prevent GitHub Pages from using Jekyll. You can turn off this behavior by setting the `githubPages` option to false. -------------------------------------------------------------------------------- /.taskmaster/state.json: -------------------------------------------------------------------------------- 1 | { 2 | "currentTag": "master", 3 | "lastSwitched": "2025-07-17T12:07:14.835Z", 4 | "branchTagMapping": {}, 5 | "migrationNoticeShown": true 6 | } -------------------------------------------------------------------------------- /packages/sdk/.graphclientrc.yml: -------------------------------------------------------------------------------- 1 | sources: 2 | - name: ensemble-ai 3 | handler: 4 | graphql: 5 | endpoint: https://api.studio.thegraph.com/query/12344/ensemble-ai/version/latest -------------------------------------------------------------------------------- /packages/contracts/ignition/params/ensemble-credits.json: -------------------------------------------------------------------------------- 1 | { 2 | "tokenName": "Ensemble Credits", 3 | "tokenSymbol": "EC", 4 | "initialAdmin": "${DEPLOYER_ADDRESS}", 5 | "initialSupply": 0 6 | } -------------------------------------------------------------------------------- /packages/contracts/ignition/deployments/chain-84532/artifacts/EnsembleCreditsModule#EnsembleCredits.dbg.json: -------------------------------------------------------------------------------- 1 | { 2 | "_format": "hh-sol-dbg-1", 3 | "buildInfo": "../build-info/40c23a112789d71e0ac92ec95a0fc229.json" 4 | } -------------------------------------------------------------------------------- /packages/contracts/ignition/deployments/chain-84532/artifacts/TaskRegistryUpgradeableModule#TaskRegistryImpl.dbg.json: -------------------------------------------------------------------------------- 1 | { 2 | "_format": "hh-sol-dbg-1", 3 | "buildInfo": "../build-info/fab9518c2cef19300334ce4b9b347f6b.json" 4 | } -------------------------------------------------------------------------------- /packages/contracts/ignition/deployments/chain-84532/artifacts/TaskRegistryUpgradeableModule#TaskRegistryProxied.dbg.json: -------------------------------------------------------------------------------- 1 | { 2 | "_format": "hh-sol-dbg-1", 3 | "buildInfo": "../build-info/fab9518c2cef19300334ce4b9b347f6b.json" 4 | } -------------------------------------------------------------------------------- /packages/contracts/ignition/deployments/chain-84532/artifacts/TaskRegistryUpgradeableModule#TaskRegistryProxy.dbg.json: -------------------------------------------------------------------------------- 1 | { 2 | "_format": "hh-sol-dbg-1", 3 | "buildInfo": "../build-info/40c23a112789d71e0ac92ec95a0fc229.json" 4 | } -------------------------------------------------------------------------------- /packages/contracts/ignition/deployments/chain-50312/artifacts/AgentsRegistryUpgradeableModule#AgentsRegistryImpl.dbg.json: -------------------------------------------------------------------------------- 1 | { 2 | "_format": "hh-sol-dbg-1", 3 | "buildInfo": "../build-info/fab9518c2cef19300334ce4b9b347f6b.json" 4 | } -------------------------------------------------------------------------------- /packages/contracts/ignition/deployments/chain-84532/artifacts/AgentsRegistryUpgradeableModule#AgentsRegistryImpl.dbg.json: -------------------------------------------------------------------------------- 1 | { 2 | "_format": "hh-sol-dbg-1", 3 | "buildInfo": "../build-info/fab9518c2cef19300334ce4b9b347f6b.json" 4 | } -------------------------------------------------------------------------------- /packages/contracts/ignition/deployments/chain-84532/artifacts/AgentsRegistryUpgradeableModule#AgentsRegistryProxied.dbg.json: -------------------------------------------------------------------------------- 1 | { 2 | "_format": "hh-sol-dbg-1", 3 | "buildInfo": "../build-info/fab9518c2cef19300334ce4b9b347f6b.json" 4 | } -------------------------------------------------------------------------------- /packages/contracts/ignition/deployments/chain-84532/artifacts/AgentsRegistryUpgradeableModule#AgentsRegistryProxy.dbg.json: -------------------------------------------------------------------------------- 1 | { 2 | "_format": "hh-sol-dbg-1", 3 | "buildInfo": "../build-info/40c23a112789d71e0ac92ec95a0fc229.json" 4 | } -------------------------------------------------------------------------------- /packages/contracts/ignition/deployments/chain-84532/artifacts/ServiceRegistryUpgradeableModule#ServiceRegistryImpl.dbg.json: -------------------------------------------------------------------------------- 1 | { 2 | "_format": "hh-sol-dbg-1", 3 | "buildInfo": "../build-info/fab9518c2cef19300334ce4b9b347f6b.json" 4 | } -------------------------------------------------------------------------------- /packages/contracts/ignition/deployments/chain-84532/artifacts/ServiceRegistryUpgradeableModule#ServiceRegistryProxy.dbg.json: -------------------------------------------------------------------------------- 1 | { 2 | "_format": "hh-sol-dbg-1", 3 | "buildInfo": "../build-info/40c23a112789d71e0ac92ec95a0fc229.json" 4 | } -------------------------------------------------------------------------------- /packages/contracts/ignition/deployments/chain-84532/artifacts/ServiceRegistryUpgradeableModule#ServiceRegistryProxied.dbg.json: -------------------------------------------------------------------------------- 1 | { 2 | "_format": "hh-sol-dbg-1", 3 | "buildInfo": "../build-info/fab9518c2cef19300334ce4b9b347f6b.json" 4 | } -------------------------------------------------------------------------------- /packages/mcp-server/.env.example: -------------------------------------------------------------------------------- 1 | # MCP Server configuration 2 | PORT=3000 3 | NODE_ENV=development 4 | 5 | # Subgraph endpoint 6 | SUBGRAPH_URL=https://api.goldsky.com/api/public/project_cm9zz5dndyzbf01tm1a1874j0/subgraphs/ensemble/0.1.0/gn 7 | -------------------------------------------------------------------------------- /packages/contracts/ignition/params/local.json: -------------------------------------------------------------------------------- 1 | { 2 | "AgentsRegistryUpgradeableModule": { 3 | "v1RegistryAddress": "0x0000000000000000000000000000000000000000" 4 | }, 5 | "TaskRegistryUpgradeableModule": { 6 | "initialTaskId": 1 7 | } 8 | } -------------------------------------------------------------------------------- /packages/contracts/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | pkill -f "hardhat node" 4 | 5 | rm -rf cache 6 | rm -rf ignition/deployments/chain-31337 7 | 8 | npx hardhat node & 9 | 10 | sleep 3 11 | 12 | npx hardhat run scripts/deploy.ts --network localhost -------------------------------------------------------------------------------- /packages/contracts/ignition/params/baseSepolia.json: -------------------------------------------------------------------------------- 1 | { 2 | "AgentsRegistryUpgradeableModule": { 3 | "v1RegistryAddress": "0x0000000000000000000000000000000000000000" 4 | }, 5 | "TaskRegistryUpgradeableModule": { 6 | "initialTaskId": 1 7 | } 8 | } -------------------------------------------------------------------------------- /packages/python-sdk/ai_agents_sdk.egg-info/PKG-INFO: -------------------------------------------------------------------------------- 1 | Metadata-Version: 2.1 2 | Name: ai-agents-sdk 3 | Version: 0.1.0 4 | Requires-Python: >=3.8 5 | Requires-Dist: web3>=6.0.0 6 | Requires-Dist: eth-typing>=3.0.0 7 | Requires-Dist: python-dotenv>=1.0.0 8 | Requires-Dist: google-cloud-pubsub>=2.27.1 9 | -------------------------------------------------------------------------------- /packages/contracts/scripts/verify.sh: -------------------------------------------------------------------------------- 1 | 2 | npx hardhat verify $SERVICE_REGISTRY_ADDRESS --network $NETWORK_NAME 3 | 4 | npx hardhat verify $AGENT_REGISTRY_ADDRESS $SERVICE_REGISTRY_ADDRESS --network $NETWORK_NAME 5 | 6 | npx hardhat verify $TASK_REGISTRY_ADDRESS $AGENT_REGISTRY_ADDRESS --network $NETWORK_NAME -------------------------------------------------------------------------------- /packages/contracts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "commonjs", 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "resolveJsonModule": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/subgraph/networks.json: -------------------------------------------------------------------------------- 1 | { 2 | "base-sepolia": { 3 | "AgentsRegistry": { 4 | "address": "0x12bE0AC7a76813368989b06051CCBA03375039d2", 5 | "startBlock": 18386859 6 | }, 7 | "TaskRegistry": { 8 | "address": "0x2067DAEc00A7A21F4DEbb1FbCB19b58b52c3211d" 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /packages/sdk/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | transform: { 5 | '^.+\\.tsx?$': 'ts-jest', 6 | }, 7 | moduleNameMapper: { 8 | // Mock graphql-request to avoid ESM issues 9 | 'graphql-request': '/__mocks__/graphql-request.js' 10 | }, 11 | testTimeout: 15000 12 | }; -------------------------------------------------------------------------------- /packages/contracts/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | 4 | # Hardhat files 5 | /cache 6 | /artifacts 7 | 8 | # TypeChain files 9 | /typechain 10 | /typechain-types 11 | 12 | # solidity-coverage files 13 | /coverage 14 | /coverage.json 15 | 16 | # Hardhat Ignition default folder for deployments against a local node 17 | ignition/deployments/chain-31337 18 | -------------------------------------------------------------------------------- /packages/contracts/contracts/interfaces/IProposalStruct.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | interface IProposalStruct { 4 | struct ServiceProposal { 5 | address issuer; 6 | string serviceName; 7 | uint256 price; 8 | address tokenAddress; 9 | uint256 proposalId; 10 | bool isActive; 11 | } 12 | } -------------------------------------------------------------------------------- /packages/sdk/src/utils/parsePrompt.ts: -------------------------------------------------------------------------------- 1 | export default function parsePrompt(prompt: string) { 2 | const params = prompt.split(".") 3 | const args: {[key: string]: string} = {} 4 | 5 | for (let param of params) { 6 | const [key, value] = param.split(":").map(part => part.trim()) 7 | args[key] = value 8 | } 9 | 10 | return args 11 | } 12 | -------------------------------------------------------------------------------- /packages/python-sdk/tests/conftest.py: -------------------------------------------------------------------------------- 1 | 2 | import pytest 3 | import asyncio 4 | 5 | pytest_plugins = ('pytest_asyncio',) 6 | 7 | def pytest_configure(config): 8 | config.option.asyncio_default_fixture_loop_scope = "function" 9 | 10 | @pytest.fixture(scope="session") 11 | def event_loop(): 12 | loop = asyncio.get_event_loop() 13 | yield loop 14 | loop.close() 15 | -------------------------------------------------------------------------------- /packages/contracts/contracts/Proxies.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | 4 | // Import OpenZeppelin proxy contracts for Hardhat Ignition 5 | import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; 6 | import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; 7 | import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; -------------------------------------------------------------------------------- /packages/mcp-server/src/subgraph/client.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLClient } from 'graphql-request'; 2 | import { getSdk } from './generated'; 3 | import { config } from '../config'; 4 | 5 | // Get subgraph URL from configuration 6 | const SUBGRAPH_URL = config.subgraph.url; 7 | 8 | export const graphQLClient = new GraphQLClient(SUBGRAPH_URL); 9 | export const subgraphClient = getSdk(graphQLClient); -------------------------------------------------------------------------------- /packages/python-sdk/ai_agents_sdk.egg-info/SOURCES.txt: -------------------------------------------------------------------------------- 1 | setup.py 2 | ai_agents_sdk.egg-info/PKG-INFO 3 | ai_agents_sdk.egg-info/SOURCES.txt 4 | ai_agents_sdk.egg-info/dependency_links.txt 5 | ai_agents_sdk.egg-info/requires.txt 6 | ai_agents_sdk.egg-info/top_level.txt 7 | src/__init__.py 8 | src/ensemble.py 9 | src/types.py 10 | tests/__init__.py 11 | tests/conftest.py 12 | tests/test_sdk.py -------------------------------------------------------------------------------- /packages/mcp-server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "Node16", 5 | "moduleResolution": "node16", 6 | "esModuleInterop": true, 7 | "strict": true, 8 | "outDir": "dist", 9 | "sourceMap": true, 10 | "declaration": true, 11 | "skipLibCheck": true, 12 | "resolveJsonModule": true, 13 | }, 14 | "include": ["src/**/*"] 15 | } -------------------------------------------------------------------------------- /.replit: -------------------------------------------------------------------------------- 1 | run = "cd packages/python-sdk && python -m pytest tests/" 2 | modules = ["python-3.11:v18-20230807-322e88b"] 3 | 4 | [nix] 5 | channel = "stable-24_05" 6 | 7 | [languages] 8 | [languages.python] 9 | pattern = "**/*.py" 10 | syntax = "python" 11 | # languageServer = "python" 12 | 13 | [deployment] 14 | run = ["python3", "main.py"] 15 | deploymentTarget = "cloudrun" 16 | 17 | [[ports]] 18 | localPort = 8545 19 | externalPort = 80 20 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.3.1/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "public", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [], 11 | "___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": { 12 | "onlyUpdatePeerDependentsWhenOutOfRange": true 13 | } 14 | } -------------------------------------------------------------------------------- /packages/cli/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | roots: ['/src'], 5 | testMatch: ['**/__tests__/**/*.test.ts', '**/?(*.)+(spec|test).ts'], 6 | transform: { 7 | '^.+\\.tsx?$': 'ts-jest', 8 | }, 9 | collectCoverageFrom: [ 10 | 'src/**/*.{ts,tsx}', 11 | '!src/**/*.d.ts', 12 | '!src/bin/**', 13 | ], 14 | moduleNameMapping: { 15 | '^@/(.*)$': '/src/$1', 16 | }, 17 | }; -------------------------------------------------------------------------------- /packages/python-sdk/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name="ai-agents-sdk", 5 | version="0.1.0", 6 | packages=find_packages(), 7 | install_requires=[ 8 | "web3>=6.0.0", 9 | "eth-typing>=3.0.0", 10 | "python-dotenv>=1.0.0", 11 | "google-cloud-pubsub>=2.27.1", 12 | ], 13 | tests_require=[ 14 | "pytest>=7.0.0", 15 | "pytest-asyncio>=0.25.0", 16 | ], 17 | python_requires=">=3.8", 18 | ) 19 | -------------------------------------------------------------------------------- /packages/sdk/scripts/create-task.ts: -------------------------------------------------------------------------------- 1 | import { setupSdk } from "./utils/setupSdk"; 2 | 3 | async function main() { 4 | const ensemble = setupSdk(); 5 | 6 | let topic = "GOAT"; 7 | let style = "exciting"; 8 | const task = await ensemble.createTask({ 9 | prompt: `Write a tweet about ${topic}. style: ${style}`, 10 | proposalId: "0", 11 | }); 12 | console.log(task); 13 | } 14 | 15 | main().catch((error) => { 16 | console.error("Error in main function:", error); 17 | process.exit(1); 18 | }); 19 | -------------------------------------------------------------------------------- /packages/contracts/contracts/interfaces/IAgent.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | 4 | interface IAgent { 5 | struct Skill { 6 | string name; 7 | uint256 level; 8 | } 9 | 10 | event ReputationUpdated(address indexed agent, uint256 newReputation); 11 | 12 | function updateReputation(uint256 _reputation) external; 13 | function getSkills() external view returns (Skill[] memory); 14 | function getReputation() external view returns (uint256); 15 | } 16 | -------------------------------------------------------------------------------- /packages/sdk/__mocks__/graphql-request.js: -------------------------------------------------------------------------------- 1 | // Mock for graphql-request to avoid ESM issues in Jest 2 | 3 | class GraphQLClient { 4 | constructor(url) { 5 | this.url = url; 6 | this.request = jest.fn(); 7 | } 8 | } 9 | 10 | function gql(strings, ...values) { 11 | // Handle template literal: combine strings and values 12 | let result = strings[0]; 13 | for (let i = 0; i < values.length; i++) { 14 | result += values[i] + strings[i + 1]; 15 | } 16 | return result; 17 | } 18 | 19 | module.exports = { 20 | GraphQLClient, 21 | gql 22 | }; -------------------------------------------------------------------------------- /packages/subgraph/.gitignore: -------------------------------------------------------------------------------- 1 | # Graph CLI generated artifacts 2 | build/ 3 | generated/ 4 | 5 | # Dependency directories 6 | node_modules/ 7 | jspm_packages/ 8 | 9 | # Logs 10 | logs 11 | *.log 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | 16 | # Optional npm cache directory 17 | .npm 18 | 19 | # Optional eslint cache 20 | .eslintcache 21 | 22 | # dotenv environment variables file 23 | .env 24 | 25 | # Testing 26 | coverage 27 | coverage.json 28 | 29 | # Typechain 30 | typechain 31 | typechain-types 32 | 33 | # Hardhat files 34 | cache 35 | -------------------------------------------------------------------------------- /packages/subgraph/src/utils.ts: -------------------------------------------------------------------------------- 1 | export let IPFS_SCHEME = "ipfs://"; 2 | 3 | export let HTTP_SCHEME = "https://"; 4 | 5 | export let BASE_IPFS_URL = "https://ipfs.io/ipfs/"; 6 | 7 | export function getContentPath(tokenURI: string): string { 8 | if (tokenURI.startsWith(HTTP_SCHEME) && tokenURI.length > HTTP_SCHEME.length) { 9 | return tokenURI.split(BASE_IPFS_URL).join("") 10 | } else if (tokenURI.startsWith(IPFS_SCHEME) && tokenURI.length > IPFS_SCHEME.length) { 11 | return tokenURI.split(IPFS_SCHEME).join("") 12 | } else { 13 | return "" 14 | } 15 | } -------------------------------------------------------------------------------- /packages/mcp-server/src/config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Configuration settings for the MCP server 3 | */ 4 | export const config = { 5 | server: { 6 | port: process.env.PORT ? parseInt(process.env.PORT) : 3000, 7 | env: process.env.NODE_ENV || 'development', 8 | }, 9 | subgraph: { 10 | url: process.env.SUBGRAPH_URL || 'https://api.goldsky.com/api/public/project_cm9zz5dndyzbf01tm1a1874j0/subgraphs/ensemble/0.1.0/gn', 11 | }, 12 | matching: { 13 | defaultLimit: 5, 14 | maxLimit: 10, 15 | minQueryLength: 3, 16 | serviceMatchBonus: 50, // Bonus score for matching a service 17 | } 18 | }; -------------------------------------------------------------------------------- /packages/sdk/scripts/register-services.ts: -------------------------------------------------------------------------------- 1 | import { servicesList } from "./data/servicesList"; 2 | import { setupSdk } from "./utils/setupSdk"; 3 | 4 | async function main() { 5 | const ensemble = setupSdk(); 6 | 7 | for (const service of servicesList) { 8 | try { 9 | await ensemble.registerService(service); 10 | } catch (error) { 11 | console.error(`Error registering service ${service.name}:`, error); 12 | } 13 | } 14 | 15 | process.stdin.resume(); 16 | } 17 | 18 | main().catch((error) => { 19 | console.error("Error in main function:", error); 20 | process.exit(1); 21 | }); 22 | -------------------------------------------------------------------------------- /packages/sdk/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "CommonJS", 4 | "target": "ES2020", 5 | "esModuleInterop": true, 6 | "allowSyntheticDefaultImports": true, 7 | "declaration": true, 8 | "declarationMap": true, 9 | "sourceMap": true, 10 | "outDir": "./dist", 11 | "strict": true, 12 | "skipLibCheck": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "resolveJsonModule": true, 15 | "moduleResolution": "node" 16 | }, 17 | "include": ["index.ts", "src/**/*", "typechain/**/*"], 18 | "exclude": ["node_modules", "dist", "**/*.test.ts"] 19 | } 20 | -------------------------------------------------------------------------------- /packages/subgraph/src/constants.ts: -------------------------------------------------------------------------------- 1 | export const blacklistedAgents = [ 2 | '0x114375c8b0a6231449c6961b0746cb0117d66f4f', 3 | '0x14cad4605c687f1da444577b0a1805036dd9b039', 4 | '0x2c37691967de1a1e4ee68ae4d745059720a6db7f', 5 | '0x515e4af972d84920a9e774881003b2bd797c4d4b', 6 | '0x57637e2534864db7406f7e8129afcf1e38374f98', 7 | '0x83df687c3642b6ac84a5083206eac69a9fd918f9', 8 | '0x94ad8743e0251bfe25c011fa86e73f4007b4fdc2', 9 | '0xabe83677b6ae8783a75667bb5cbd836cbd3605c9', 10 | '0xe03ce825669af732a59ae4dbf2f95c5caed48a23', 11 | '0xf74faaeee3be756d2e5238e2e067f33a3cf0ba36', 12 | '0xfdcb66224f433f3f7bff246571c1c26b071ed952' 13 | ] 14 | -------------------------------------------------------------------------------- /packages/sdk/index.ts: -------------------------------------------------------------------------------- 1 | import { TaskService } from "./src/services/TaskService"; 2 | import { AgentService } from "./src/services/AgentService"; 3 | import { ContractService } from "./src/services/ContractService"; 4 | import { ServiceRegistryService } from "./src/services/ServiceRegistryService"; 5 | import Ensemble from "./src/ensemble"; 6 | 7 | // Export all types from the SDK 8 | export * from "./src/types"; 9 | 10 | // Export services 11 | export { 12 | Ensemble, 13 | TaskService, 14 | AgentService, 15 | ContractService, 16 | ServiceRegistryService 17 | }; 18 | 19 | // Export errors 20 | export * from "./src/errors"; 21 | 22 | export default Ensemble; -------------------------------------------------------------------------------- /packages/cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "commonjs", 5 | "lib": ["ES2022"], 6 | "outDir": "./dist", 7 | "rootDir": "./src", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "resolveJsonModule": true, 13 | "declaration": true, 14 | "declarationMap": true, 15 | "sourceMap": true, 16 | "moduleResolution": "node", 17 | "allowSyntheticDefaultImports": true 18 | }, 19 | "include": [ 20 | "src/**/*" 21 | ], 22 | "exclude": [ 23 | "node_modules", 24 | "dist", 25 | "**/*.test.ts" 26 | ] 27 | } -------------------------------------------------------------------------------- /packages/sdk/.env.example: -------------------------------------------------------------------------------- 1 | # Network Configuration 2 | RPC_URL=https://sepolia.base.org 3 | CHAIN_ID=84532 4 | 5 | # Contract Addresses (replace with actual deployed addresses) 6 | AGENT_REGISTRY_ADDRESS=0xDbF645cC23066cc364C4Db915c78135eE52f11B2 7 | SERVICE_REGISTRY_ADDRESS=0x3Acbf1Ca047a18bE88E7160738A9B0bB64203244 8 | TASK_REGISTRY_ADDRESS=0x847fA49b999489fD2780fe2843A7b1608106b49b 9 | 10 | # Subgraph URL (replace with actual subgraph URL) 11 | SUBGRAPH_URL=https://api.thegraph.com/subgraphs/name/your-subgraph 12 | 13 | # Test Data (optional - for integration tests) 14 | TEST_OWNER_ADDRESS=0x1234567890123456789012345678901234567890 15 | TEST_AGENT_ADDRESS=0x0987654321098765432109876543210987654321 -------------------------------------------------------------------------------- /packages/contracts/contracts/interfaces/ITask.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | 4 | interface ITask { 5 | enum TaskType { SIMPLE, COMPLEX, COMPOSITE } 6 | enum TaskStatus { CREATED, ASSIGNED, COMPLETED, FAILED } 7 | 8 | event TaskExecuted(uint256 indexed taskId, bool success); 9 | event TaskStatusChanged(uint256 indexed taskId, TaskStatus status); 10 | event TaskAssigned(uint256 indexed taskId, address assignee); 11 | 12 | function execute(bytes calldata data, address target, uint256 value) external returns (bool); 13 | function getStatus() external view returns (TaskStatus); 14 | function getAssignee() external view returns (address); 15 | } 16 | -------------------------------------------------------------------------------- /packages/python-sdk/src/services/contract_service.py: -------------------------------------------------------------------------------- 1 | 2 | from web3 import Web3 3 | from eth_typing import Address 4 | from typing import Any 5 | 6 | class ContractService: 7 | def __init__(self, provider: Web3, account: Address): 8 | self.provider = provider 9 | self.account = account 10 | 11 | async def validate_network(self, expected_chain_id: int) -> None: 12 | network = await self.provider.eth.chain_id 13 | if network != expected_chain_id: 14 | raise Exception(f"Chain ID mismatch. Expected {expected_chain_id}, got {network}") 15 | 16 | def create_contract(self, address: str, abi: Any) -> Any: 17 | return self.provider.eth.contract(address=address, abi=abi) 18 | -------------------------------------------------------------------------------- /packages/mcp-server/jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'jest'; 2 | 3 | const config: Config = { 4 | preset: 'ts-jest', 5 | testEnvironment: 'node', 6 | testMatch: ['**/tests/**/*.test.ts'], 7 | verbose: true, 8 | collectCoverage: true, 9 | coverageDirectory: 'coverage', 10 | collectCoverageFrom: [ 11 | 'src/**/*.ts', 12 | '!src/**/*.d.ts', 13 | ], 14 | transform: { 15 | '^.+\\.tsx?$': ['ts-jest', { 16 | tsconfig: 'tsconfig.json', 17 | }], 18 | }, 19 | moduleFileExtensions: ['ts', 'js', 'json', 'node'], 20 | globals: { 21 | 'ts-jest': { 22 | isolatedModules: true, 23 | }, 24 | }, 25 | testTimeout: 30000, // Increased timeout for tests that call the real subgraph 26 | }; 27 | 28 | export default config; -------------------------------------------------------------------------------- /packages/sdk/src/services/ContractService.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "ethers"; 2 | 3 | export class ContractService { 4 | private provider: ethers.Provider; 5 | private signer: ethers.Signer; 6 | 7 | constructor(provider: ethers.Provider, signer: ethers.Signer) { 8 | this.provider = provider; 9 | this.signer = signer; 10 | } 11 | 12 | async validateNetwork(expectedChainId: number): Promise { 13 | const network = await this.provider.getNetwork(); 14 | if (network.chainId !== BigInt(expectedChainId)) { 15 | throw new Error(`Chain ID mismatch. Expected ${expectedChainId}, got ${network.chainId}`); 16 | } 17 | } 18 | 19 | createContract(address: string, abi: any): ethers.Contract { 20 | return new ethers.Contract(address, abi, this.signer); 21 | } 22 | } -------------------------------------------------------------------------------- /packages/subgraph/src/service-registry.ts: -------------------------------------------------------------------------------- 1 | import { ServiceRegistered, ServiceUpdated } from '../generated/ServiceRegistry/ServiceRegistry' 2 | import { Service } from '../generated/schema' 3 | 4 | export function handleServiceRegistered(event: ServiceRegistered): void { 5 | let entity = new Service(event.params.name); 6 | 7 | entity.name = event.params.name; 8 | entity.description = event.params.description; 9 | entity.category = event.params.category; 10 | 11 | entity.save(); 12 | } 13 | 14 | export function handleServiceUpdated(event: ServiceUpdated): void { 15 | let entity = Service.load(event.params.name); 16 | if (entity == null) { 17 | return 18 | } 19 | 20 | entity.name = event.params.name; 21 | entity.description = event.params.description; 22 | 23 | entity.save(); 24 | } -------------------------------------------------------------------------------- /packages/sdk/src/index.ts: -------------------------------------------------------------------------------- 1 | import Ensemble from "./ensemble" 2 | import { AgentService } from "./services/AgentService" 3 | import { TaskService } from "./services/TaskService" 4 | import { ContractService } from "./services/ContractService" 5 | import { ServiceRegistryService } from "./services/ServiceRegistryService" 6 | 7 | // Export all types and interfaces 8 | export * from "./types" 9 | 10 | // Export validation functions 11 | export { 12 | validateAgentRecord, 13 | validateRegisterParams, 14 | validateUpdateParams, 15 | validateCommunicationParams, 16 | parseAgentRecord, 17 | parseRegisterParams, 18 | parseUpdateParams 19 | } from "./schemas/agent.schemas" 20 | 21 | export { 22 | Ensemble, 23 | AgentService, 24 | TaskService, 25 | ContractService, 26 | ServiceRegistryService 27 | } 28 | 29 | export default Ensemble 30 | -------------------------------------------------------------------------------- /packages/contracts/contracts/interfaces/IAgentRegistryV1.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | 4 | interface IAgentRegistryV1 { 5 | struct AgentData { 6 | string name; 7 | string agentUri; 8 | address owner; 9 | address agent; 10 | uint256 reputation; 11 | uint256 totalRatings; 12 | } 13 | 14 | struct Proposal { 15 | address issuer; 16 | string serviceName; 17 | uint256 price; 18 | uint256 proposalId; 19 | bool isActive; 20 | } 21 | 22 | function getAgentData(address _agent) external view returns (AgentData memory); 23 | 24 | function getProposal(uint256 _proposalId) external view returns (Proposal memory); 25 | 26 | function nextProposalId() external view returns (uint256); 27 | 28 | function serviceRegistry() external view returns (address); 29 | } 30 | -------------------------------------------------------------------------------- /packages/mcp-server/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependency directories 2 | node_modules/ 3 | .pnpm-store/ 4 | 5 | # TypeScript output 6 | dist/ 7 | build/ 8 | *.tsbuildinfo 9 | 10 | # Test coverage 11 | coverage/ 12 | .nyc_output/ 13 | 14 | # Logs 15 | logs/ 16 | *.log 17 | npm-debug.log* 18 | yarn-debug.log* 19 | yarn-error.log* 20 | pnpm-debug.log* 21 | 22 | # Environment variables 23 | .env 24 | .env.local 25 | .env.development.local 26 | .env.test.local 27 | .env.production.local 28 | 29 | # Cache directories 30 | .npm/ 31 | .eslintcache 32 | .cache/ 33 | 34 | # Editor directories and files 35 | .idea/ 36 | .vscode/* 37 | !.vscode/extensions.json 38 | !.vscode/settings.json 39 | !.vscode/tasks.json 40 | !.vscode/launch.json 41 | *.code-workspace 42 | .DS_Store 43 | *.suo 44 | *.ntvs* 45 | *.njsproj 46 | *.sln 47 | *.sw? 48 | 49 | # Debug files 50 | .chrome/ 51 | .inspector/ 52 | 53 | # Temporary files 54 | tmp/ 55 | temp/ 56 | *.tmp -------------------------------------------------------------------------------- /packages/cli/src/utils/wallet.ts: -------------------------------------------------------------------------------- 1 | import { getActiveWallet } from '../config/manager'; 2 | 3 | /** 4 | * Get the effective wallet to use for operations. 5 | * Priority: global --wallet option > active wallet > undefined 6 | */ 7 | export async function getEffectiveWallet(globalWalletOption?: string): Promise { 8 | if (globalWalletOption) { 9 | return globalWalletOption; 10 | } 11 | 12 | return await getActiveWallet(); 13 | } 14 | 15 | /** 16 | * Get the effective wallet or throw an error with helpful message 17 | */ 18 | export async function getEffectiveWalletOrThrow(globalWalletOption?: string): Promise { 19 | const wallet = await getEffectiveWallet(globalWalletOption); 20 | 21 | if (!wallet) { 22 | throw new Error('No wallet specified and no active wallet set. Use --wallet or set an active wallet with "ensemble wallet use "'); 23 | } 24 | 25 | return wallet; 26 | } -------------------------------------------------------------------------------- /packages/sdk/docs/assets/navigation.js: -------------------------------------------------------------------------------- 1 | window.navigationData = "eJyV199P2zAQB/D/JXtFAzrYBm+oKxLSQChl0iS0hyM+glXHjmynI0z871OaZs0P5857vq8/dm3nkj7+STy++uQyeQC3WXvwlUuOkhL8S3KZoK4Kd3yofHzxhUqOko3UIrn8+n70b/RVjtpfKYsg6hRz6TxaFCtrjT1wmQLn0B0T4eEMp4vJHHfGX5tKk/QgEyPGrXgaZO012q3McAbcV1nlRynAI7W0XoLSlkZ7C9n8skYBylpph8WTCiBdhRp9o7egpNgt/mZu1wMhyry3pjQOFHNFgjHK3W9G7O0m4xHzxNzH2WiE346w9ewlCOcoedch5rhekbzpQnQncw8Wil4bktqjfYasue3j1JBcnH+eNCXnZK5RrLaofdicxFj0G3iYt5oqS1xL5dFSP3WcosjumVsa/SzzoDeMUNgd+t/GbghrkKCo7rCCSleMAa6EoM5wGotCy9KabZTbT8bQKRZxcj8YA7etPgLuByk4xbLy4KXRLB2OUvikK/S0YFMYDd9IFb47uwo1tGk7/LM/SXHkEnSGiiMHKZY0RamQ3PhpjEe1xswb273SCXeUZGmL9DUZh6JAaTTRDKcxDp1t0V2RA1avmFXNfKQ0SHFkyu9bGr1r7Sf58gV0zqHTKIlb0A6y5kel6Co1445T7BtvaYqi0jLbHeJDXfa6gq/L7o03SY3ck4svp+eLsX2LHsTgnHpkV4ySUsyMFUGnLf2H0rRitL4mtC4Spa5NJkG5ILevxTmj/3l9JvBHb6oMDmn82LZaIMKpKyXfIKz1SpzyHXPIauJzshVnYpzefW/vNissByKcukYrQck3FBE7S4a5mdoXNzwpJG57MMTJP28f7sMLPlQ4Q+AzDBpOYUSlsB334VBtx5+dXpx9Ojl7//UXHULNcw==" -------------------------------------------------------------------------------- /packages/cli/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @ensemble-ai/cli 2 | 3 | ## 0.1.1 4 | 5 | ### Patch Changes 6 | 7 | - 02eed68: fix: Make subgraphUrl required parameter in SDK configuration 8 | - SDK: subgraphUrl is now required in EnsembleConfig interface 9 | - SDK: AgentService constructor now requires subgraphUrl parameter 10 | - SDK: Updated parameter order in AgentService constructor (subgraphUrl comes after registry) 11 | - SDK: Removed conditional initialization of GraphQL client - now always initialized 12 | - CLI: Already provides subgraphUrl from config, no breaking changes for CLI users 13 | - Tests: Updated all test configurations to include subgraphUrl 14 | - Documentation: Updated README examples with required subgraphUrl parameter 15 | 16 | This ensures that agent query methods (`getAgentRecords`, `getAgentsByOwner`, etc.) always work correctly by guaranteeing subgraph connectivity. 17 | 18 | - Updated dependencies [02eed68] 19 | - @ensemble-ai/sdk@0.6.2 20 | -------------------------------------------------------------------------------- /packages/sdk/scripts/register-agents.ts: -------------------------------------------------------------------------------- 1 | import { agentsList } from "./data/agentsList"; 2 | import { setupSdk } from "./utils/setupSdk"; 3 | 4 | async function main() { 5 | const ensemble = setupSdk(); 6 | 7 | for (const agent of agentsList) { 8 | console.log(`Registering agent ${agent.name} with address ${agent.address}`); 9 | await ensemble.registerAgent( 10 | agent.address, 11 | { 12 | name: agent.name, 13 | description: agent.description, 14 | agentCategory: agent.agentCategory, 15 | instructions: agent.instructions, 16 | prompts: agent.prompts, 17 | imageURI: agent.imageURI, 18 | socials: agent.socials, 19 | attributes: agent.attributes, 20 | communicationType: agent.communicationType 21 | }, 22 | ); 23 | } 24 | 25 | process.stdin.resume(); 26 | } 27 | 28 | main().catch((error) => { 29 | console.error("Error in main function:", error); 30 | process.exit(1); 31 | }); 32 | -------------------------------------------------------------------------------- /.taskmaster/tasks/task_003.txt: -------------------------------------------------------------------------------- 1 | # Task ID: 3 2 | # Title: Build TypeScript SDK for Agent Integration 3 | # Status: done 4 | # Dependencies: 1 5 | # Priority: high 6 | # Description: Develop comprehensive TypeScript SDK providing APIs for agent registration, task discovery, proposal submission, and payment management 7 | # Details: 8 | Create @ensemble/sdk package with classes: EnsembleClient, AgentManager, TaskManager, PaymentManager. Implement Web3 integration using ethers.js v6 for Base network connectivity. Provide async/await APIs for registerAgent(), discoverTasks(), submitProposal(), executeTask(). Include real-time WebSocket connections for task notifications and status updates. Support wallet integration (MetaMask, WalletConnect) and environment configuration for mainnet/testnet. 9 | 10 | # Test Strategy: 11 | Unit tests for all SDK methods with mocked blockchain interactions. Integration tests against deployed testnet contracts. End-to-end testing with sample agent implementations. Performance testing for high-frequency operations. 12 | -------------------------------------------------------------------------------- /packages/cli/src/types/config.ts: -------------------------------------------------------------------------------- 1 | export interface CLIConfig { 2 | network: 'mainnet' | 'sepolia' | 'baseSepolia'; 3 | rpcUrl: string; 4 | privateKey?: string; 5 | gasPrice: string; 6 | outputFormat: 'table' | 'json' | 'csv' | 'yaml'; 7 | activeWallet?: string; 8 | contracts: { 9 | agentRegistry: string; 10 | taskRegistry: string; 11 | serviceRegistry: string; 12 | }; 13 | subgraphUrl?: string; 14 | pinata?: { 15 | jwt?: string; 16 | gateway?: string; 17 | }; 18 | } 19 | 20 | export interface AgentRecordYAML { 21 | name: string; 22 | description: string; 23 | category: string; 24 | attributes?: string[]; 25 | instructions?: string[]; 26 | prompts?: string[]; 27 | imageURI?: string; 28 | communication?: { 29 | type: 'eliza' | 'xmtp'; 30 | params?: Record; 31 | }; 32 | socials?: { 33 | twitter?: string; 34 | telegram?: string; 35 | github?: string; 36 | website?: string; 37 | dexscreener?: string; 38 | }; 39 | status?: 'active' | 'inactive' | 'maintenance'; 40 | } -------------------------------------------------------------------------------- /packages/sdk/scripts/create-proposals.ts: -------------------------------------------------------------------------------- 1 | import { proposalsList } from "./data/proposalsList"; 2 | import { setupSdk } from "./utils/setupSdk"; 3 | 4 | async function main() { 5 | const ensemble = setupSdk(); 6 | 7 | console.log(`Creating ${proposalsList.length} proposals...`); 8 | 9 | for (const proposal of proposalsList) { 10 | try { 11 | console.log(`Creating proposal for agent ${proposal.agentAddress} for service ${proposal.serviceName}...`); 12 | await ensemble.addProposal( 13 | proposal.agentAddress, 14 | proposal.serviceName, 15 | Number(proposal.servicePrice), 16 | proposal.tokenAddress 17 | ); 18 | console.log(`✓ Successfully created proposal for ${proposal.serviceName}`); 19 | } catch (error) { 20 | console.error(`✗ Error creating proposal for ${proposal.serviceName}:`, error); 21 | } 22 | } 23 | 24 | console.log("Finished creating proposals."); 25 | process.stdin.resume(); 26 | } 27 | 28 | main().catch((error) => { 29 | console.error("Error in main function:", error); 30 | process.exit(1); 31 | }); -------------------------------------------------------------------------------- /.taskmaster/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "models": { 3 | "main": { 4 | "provider": "anthropic", 5 | "modelId": "claude-sonnet-4-20250514", 6 | "maxTokens": 64000, 7 | "temperature": 0.2 8 | }, 9 | "research": { 10 | "provider": "perplexity", 11 | "modelId": "sonar-pro", 12 | "maxTokens": 8700, 13 | "temperature": 0.1 14 | }, 15 | "fallback": { 16 | "provider": "anthropic", 17 | "modelId": "claude-3-7-sonnet-20250219", 18 | "maxTokens": 120000, 19 | "temperature": 0.2 20 | } 21 | }, 22 | "global": { 23 | "logLevel": "info", 24 | "debug": false, 25 | "defaultNumTasks": 10, 26 | "defaultSubtasks": 5, 27 | "defaultPriority": "medium", 28 | "projectName": "Taskmaster", 29 | "ollamaBaseURL": "http://localhost:11434/api", 30 | "bedrockBaseURL": "https://bedrock.us-east-1.amazonaws.com", 31 | "responseLanguage": "English", 32 | "defaultTag": "master", 33 | "azureOpenaiBaseURL": "https://your-endpoint.openai.azure.com/", 34 | "userId": "1234567890" 35 | }, 36 | "claudeCode": {} 37 | } -------------------------------------------------------------------------------- /packages/sdk/scripts/data/agentsList.ts: -------------------------------------------------------------------------------- 1 | import { AgentCommunicationType } from "../../src/types"; 2 | 3 | export const agentsList = [ 4 | { 5 | name: "Onii-Chan influencer", 6 | description: "Onii-Chan is a popular influencer on social media.", 7 | imageURI: "https://ipfs.io/ipfs/bafkreigzpb44ndvlsfazfymmf6yvquoregceik56vyskf7e35joel7yati", 8 | owner: "0x515e4af972D84920a9e774881003b2bD797c4d4b", 9 | address: "0x114375c8B0A6231449c6961b0746cB0117D66f4F", 10 | agentCategory: "Influencer", 11 | instructions: ["You are a popular influencer on social media.", "You are known for your unique style and personality."], 12 | prompts: ["You are a popular influencer on social media.", "You are known for your unique style and personality."], 13 | serviceName: "Bull-Post", 14 | servicePrice: 10000000000000000, 15 | communicationType: "xmtp" as AgentCommunicationType, 16 | communicationParams: { address: "0x114375c8B0A6231449c6961b0746cB0117D66f4F", env: "production" as const }, 17 | socials: { 18 | dexscreener: '', 19 | twitter: '', 20 | telegram: '' 21 | }, 22 | attributes: [] 23 | } 24 | ] -------------------------------------------------------------------------------- /.taskmaster/tasks/task_001.txt: -------------------------------------------------------------------------------- 1 | # Task ID: 1 2 | # Title: Deploy Core Smart Contracts on Base Networks 3 | # Status: done 4 | # Dependencies: None 5 | # Priority: high 6 | # Description: Deploy the foundational smart contracts for Service Registry, Agent Registry, and Task Registry on Base mainnet and Sepolia testnet with upgradeable patterns 7 | # Details: 8 | Implement and deploy smart contracts using OpenZeppelin's upgradeable proxy patterns. Create ServiceRegistry.sol for service catalog management, AgentRegistry.sol for agent registration and reputation tracking, and TaskRegistry.sol for decentralized task mempool. Include escrow functionality for payments and multi-token support. Use Hardhat for deployment with network-specific configurations for Base and Base Sepolia. Implement gas optimization techniques and emergency pause mechanisms. 9 | 10 | # Test Strategy: 11 | Deploy to testnet first with comprehensive unit tests using Hardhat. Test upgrade mechanisms, gas consumption analysis, and integration testing with mock scenarios. Perform security audit simulation and stress testing with high-frequency operations. 12 | -------------------------------------------------------------------------------- /packages/contracts/ignition/deployments/chain-84532/deployed_addresses.json: -------------------------------------------------------------------------------- 1 | { 2 | "AgentsRegistryUpgradeableModule#AgentsRegistryImpl": "0xAb38118AD1768baAa59bDB982029F08750274364", 3 | "EnsembleCreditsModule#EnsembleCredits": "0x5C1fef4B208f56C6ebdF4e9dD52364CE5f921DD8", 4 | "ServiceRegistryUpgradeableModule#ServiceRegistryImpl": "0x8C19A576158de60Cab40207C1d2E4D1D7fb43F31", 5 | "TaskRegistryUpgradeableModule#TaskRegistryImpl": "0x89c33DEdd6e579131FbD3fCD07513653a0E812eD", 6 | "ServiceRegistryUpgradeableModule#ServiceRegistryProxy": "0x05506f4eA523C103820b4733663Bbbd0D119806f", 7 | "ServiceRegistryUpgradeableModule#ServiceRegistryProxied": "0x05506f4eA523C103820b4733663Bbbd0D119806f", 8 | "AgentsRegistryUpgradeableModule#AgentsRegistryProxy": "0xdc9Cdb9A7Bc2CE2bF19dEc0058dd587DE0456EE9", 9 | "AgentsRegistryUpgradeableModule#AgentsRegistryProxied": "0xdc9Cdb9A7Bc2CE2bF19dEc0058dd587DE0456EE9", 10 | "TaskRegistryUpgradeableModule#TaskRegistryProxy": "0xa16e7A279931712a651822a8A4035B5733c7f811", 11 | "TaskRegistryUpgradeableModule#TaskRegistryProxied": "0xa16e7A279931712a651822a8A4035B5733c7f811" 12 | } 13 | -------------------------------------------------------------------------------- /packages/sdk/src/errors.ts: -------------------------------------------------------------------------------- 1 | export class ServiceNotRegisteredError extends Error { 2 | constructor(serviceName: string) { 3 | super(`Service "${serviceName}" is not registered.`); 4 | this.name = "ServiceNotRegisteredError"; 5 | } 6 | } 7 | 8 | export class AgentAlreadyRegisteredError extends Error { 9 | constructor(agentAddress: string) { 10 | super(`Agent "${agentAddress}" is already registered.`); 11 | this.name = "AgentAlreadyRegisteredError"; 12 | } 13 | } 14 | 15 | export class AgentNotRegisteredError extends Error { 16 | constructor(agentAddress: string) { 17 | super(`Agent "${agentAddress}" is not registered.`); 18 | this.name = "AgentNotRegisteredError"; 19 | } 20 | } 21 | 22 | export class ServiceAlreadyRegisteredError extends Error { 23 | constructor(serviceName: string) { 24 | super(`Service "${serviceName}" is already registered.`); 25 | this.name = "ServiceAlreadyRegisteredError"; 26 | } 27 | } 28 | 29 | export class ProposalNotFoundError extends Error { 30 | constructor(proposalId: number) { 31 | super(`Proposal "${proposalId}" not found.`); 32 | this.name = "ProposalNotFoundError"; 33 | } 34 | } -------------------------------------------------------------------------------- /packages/cli/.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled output 2 | /dist 3 | /build 4 | *.tsbuildinfo 5 | 6 | # Dependencies 7 | node_modules/ 8 | .pnpm-store/ 9 | 10 | # IDE files 11 | .vscode/ 12 | .idea/ 13 | *.swp 14 | *.swo 15 | 16 | # OS generated files 17 | .DS_Store 18 | .DS_Store? 19 | ._* 20 | .Spotlight-V100 21 | .Trashes 22 | ehthumbs.db 23 | Thumbs.db 24 | 25 | # Logs 26 | logs 27 | *.log 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | pnpm-debug.log* 32 | lerna-debug.log* 33 | 34 | # Environment variables 35 | .env 36 | .env.local 37 | .env.development.local 38 | .env.test.local 39 | .env.production.local 40 | 41 | # Testing 42 | /coverage 43 | /.nyc_output 44 | 45 | # Temporary files 46 | *.tmp 47 | *.temp 48 | .cache/ 49 | 50 | # Runtime data 51 | pids 52 | *.pid 53 | *.seed 54 | *.pid.lock 55 | 56 | # Optional npm cache directory 57 | .npm 58 | 59 | # Optional eslint cache 60 | .eslintcache 61 | 62 | # Microbundle cache 63 | .rpt2_cache/ 64 | .rts2_cache_cjs/ 65 | .rts2_cache_es/ 66 | .rts2_cache_umd/ 67 | 68 | # Optional REPL history 69 | .node_repl_history 70 | 71 | # Output of 'npm pack' 72 | *.tgz 73 | 74 | # Yarn Integrity file 75 | .yarn-integrity 76 | 77 | # CLI specific 78 | test-agent-record.yaml -------------------------------------------------------------------------------- /.taskmaster/tasks/task_006.txt: -------------------------------------------------------------------------------- 1 | # Task ID: 6 2 | # Title: Build Frontend Task Management Interface 3 | # Status: cancelled 4 | # Dependencies: None 5 | # Priority: low 6 | # Description: DEPRECATED: Frontend functionality already exists in separate agent hub dapp repository - this task is no longer needed 7 | # Details: 8 | This task has been deprecated as the frontend functionality is already implemented in a separate repository containing the agent hub dapp. The existing frontend provides the necessary web application features for task creation, agent monitoring, and marketplace interaction. No additional frontend development is required for this project. 9 | 10 | # Test Strategy: 11 | No testing required - task deprecated due to existing implementation in separate repository 12 | 13 | # Subtasks: 14 | ## 1. Document existing frontend repository location [pending] 15 | ### Dependencies: None 16 | ### Description: Document the location and details of the existing agent hub dapp repository that provides the frontend functionality 17 | ### Details: 18 | 19 | 20 | ## 2. Verify frontend integration compatibility [pending] 21 | ### Dependencies: None 22 | ### Description: Ensure the existing agent hub dapp can properly integrate with the backend services being developed in this project 23 | ### Details: 24 | 25 | 26 | -------------------------------------------------------------------------------- /packages/subgraph/schema.graphql: -------------------------------------------------------------------------------- 1 | type Service @entity { 2 | id: ID! 3 | name: String! 4 | category: String! 5 | description: String! 6 | } 7 | 8 | type Agent @entity { 9 | id: ID! 10 | name: String! 11 | agentUri: String! 12 | owner: Bytes! 13 | reputation: BigInt! 14 | metadata: IpfsMetadata 15 | tasks: [Task!]! @derivedFrom(field: "assignee") 16 | proposals: [Proposal!]! @derivedFrom(field: "issuer") 17 | } 18 | 19 | type Task @entity { 20 | id: ID! 21 | taskId: Int! 22 | prompt: String! 23 | issuer: Bytes! 24 | status: String! 25 | assignee: Agent! 26 | proposalId: BigInt! 27 | result: String 28 | rating: BigInt! 29 | } 30 | 31 | type Proposal @entity { 32 | id: ID! 33 | issuer: Agent! 34 | service: String! 35 | price: BigInt! 36 | isRemoved: Boolean! 37 | tokenAddress: Bytes! 38 | } 39 | 40 | type IpfsMetadata @entity { 41 | id: ID! 42 | name: String! 43 | description: String! 44 | agentCategory: String! 45 | openingGreeting: String! 46 | attributes: [String!]! 47 | instructions: [String!]! 48 | prompts: [String!]! 49 | communicationType: String! 50 | communicationURL: String 51 | communicationParams: String 52 | imageUri: String! 53 | twitter: String 54 | telegram: String 55 | dexscreener: String 56 | github: String 57 | website: String 58 | } 59 | -------------------------------------------------------------------------------- /packages/subgraph/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ensemble-ai/subgraph", 3 | "version": "0.1.0", 4 | "description": "The Graph subgraph for indexing Ensemble Framework events", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/ensemble-codes/ensemble-framework.git", 8 | "directory": "packages/subgraph" 9 | }, 10 | "publishConfig": { 11 | "access": "public", 12 | "registry": "https://registry.npmjs.org/" 13 | }, 14 | "license": "MIT", 15 | "homepage": "https://github.com/ensemble-codes/ensemble-framework/tree/main/packages/subgraph", 16 | "bugs": { 17 | "url": "https://github.com/ensemble-codes/ensemble-framework/issues" 18 | }, 19 | "scripts": { 20 | "codegen": "graph codegen", 21 | "build": "graph codegen && graph build", 22 | "deploy": "graph deploy ensemble-v2", 23 | "create-local": "graph create --node http://localhost:8020/ ensemble-hub", 24 | "remove-local": "graph remove --node http://localhost:8020/ ensemble-hub", 25 | "deploy-local": "graph deploy --node http://localhost:8020/ --ipfs http://localhost:5001 ensemble-hub", 26 | "test": "graph test" 27 | }, 28 | "dependencies": { 29 | "@graphprotocol/graph-cli": "0.95.0", 30 | "@graphprotocol/graph-ts": "0.32.0" 31 | }, 32 | "devDependencies": { 33 | "matchstick-as": "0.5.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/contracts/ignition/modules/EnsembleCredits.ts: -------------------------------------------------------------------------------- 1 | import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; 2 | 3 | /** 4 | * Ignition deployment module for EnsembleCredits token. 5 | * 6 | * Parameters (all optional & overridable via CLI / ignition.json): 7 | * - tokenName (string) : ERC20 name (default: "Ensemble Credits") 8 | * - tokenSymbol (string) : ERC20 symbol (default: "EC") 9 | * - initialAdmin (address) : Admin & initial minter (default: deployer[0]) 10 | * - initialSupply (uint256) : Initial supply (default: 0) 11 | */ 12 | export default buildModule("EnsembleCreditsModule", (m) => { 13 | // Parameters with sensible defaults – can be overridden at deploy-time 14 | const tokenName = m.getParameter("tokenName", "Ensemble Credits"); 15 | const tokenSymbol = m.getParameter("tokenSymbol", "EC"); 16 | const initialAdmin = m.getParameter("initialAdmin", m.getAccount(0)); 17 | const initialSupply = m.getParameter("initialSupply", 0); 18 | 19 | // Deploy contract 20 | const ensembleCredits = m.contract("EnsembleCredits", [ 21 | tokenName, 22 | tokenSymbol, 23 | initialAdmin, 24 | initialSupply, 25 | ]); 26 | 27 | // Optional: simple post-deploy call to assert deployer is minter (gas-less when simulated) 28 | m.call(ensembleCredits, "isMinter", [initialAdmin]); 29 | 30 | return { ensembleCredits }; 31 | }); -------------------------------------------------------------------------------- /.github/workflows/pr-validation.yml: -------------------------------------------------------------------------------- 1 | name: PR Validation 2 | 3 | on: 4 | pull_request: 5 | types: [opened, synchronize, reopened] 6 | 7 | jobs: 8 | validate: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout code 12 | uses: actions/checkout@v4 13 | with: 14 | fetch-depth: 0 15 | 16 | - name: Setup pnpm 17 | uses: pnpm/action-setup@v2 18 | with: 19 | version: 8 20 | 21 | - name: Setup Node.js 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: 20 25 | cache: 'pnpm' 26 | 27 | - name: Install dependencies 28 | run: pnpm install --frozen-lockfile 29 | 30 | - name: Type check 31 | run: pnpm typecheck 32 | 33 | - name: Build packages 34 | run: pnpm build 35 | 36 | - name: Run tests 37 | run: pnpm test 38 | 39 | - name: Check for changeset (skip for dependabot) 40 | if: github.actor != 'dependabot[bot]' 41 | run: | 42 | if [ "$(pnpm changeset status --verbose | grep 'No changesets present')" ]; then 43 | echo "::error::Please add a changeset by running 'pnpm changeset'" 44 | echo "::error::This helps us track changes and generate release notes" 45 | exit 1 46 | else 47 | echo "✅ Changeset found" 48 | fi -------------------------------------------------------------------------------- /packages/contracts/ignition/modules/ServiceRegistryUpgradeable.ts: -------------------------------------------------------------------------------- 1 | import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; 2 | 3 | const ServiceRegistryUpgradeableModule = buildModule("ServiceRegistryUpgradeableModule", (m) => { 4 | // Get the proxy admin owner (first account) 5 | const proxyAdminOwner = m.getAccount(0); 6 | 7 | // Deploy the ServiceRegistryUpgradeable implementation 8 | const serviceRegistryImpl = m.contract("ServiceRegistryUpgradeable", [], { 9 | id: "ServiceRegistryImpl" 10 | }); 11 | 12 | // Encode the initialize function call (no parameters needed) 13 | const initializeData = m.encodeFunctionCall(serviceRegistryImpl, "initialize", []); 14 | 15 | // Deploy the UUPS proxy with the implementation and initialization data 16 | const serviceRegistryProxy = m.contract("ERC1967Proxy", [ 17 | serviceRegistryImpl, 18 | initializeData, 19 | ], { 20 | id: "ServiceRegistryProxy" 21 | }); 22 | 23 | // Create a contract instance that uses the proxy address but the implementation ABI 24 | const serviceRegistry = m.contractAt("ServiceRegistryUpgradeable", serviceRegistryProxy, { 25 | id: "ServiceRegistryProxied" 26 | }); 27 | 28 | return { 29 | serviceRegistry, 30 | serviceRegistryProxy, 31 | serviceRegistryImpl 32 | }; 33 | }); 34 | 35 | export default ServiceRegistryUpgradeableModule; -------------------------------------------------------------------------------- /packages/mcp-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ensemble-ai/mcp-server", 3 | "version": "0.1.3", 4 | "description": "MCP server for Ensemble agent matching", 5 | "main": "dist/src/index.js", 6 | "bin": "dist/src/index.js", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/ensemble-codes/ensemble-framework.git", 10 | "directory": "packages/mcp-server" 11 | }, 12 | "publishConfig": { 13 | "access": "public", 14 | "registry": "https://registry.npmjs.org/" 15 | }, 16 | "license": "MIT", 17 | "homepage": "https://github.com/ensemble-codes/ensemble-framework/tree/main/packages/mcp-server", 18 | "bugs": { 19 | "url": "https://github.com/ensemble-codes/ensemble-framework/issues" 20 | }, 21 | "scripts": { 22 | "clean": "rm -rf dist", 23 | "build": "tsc", 24 | "dev": "ts-node src/index.ts", 25 | "start": "node dist/src/index.js", 26 | "test": "jest --config jest.config.ts" 27 | }, 28 | "dependencies": { 29 | "@graphql-typed-document-node/core": "^3.2.0", 30 | "@modelcontextprotocol/sdk": "1.10.2", 31 | "graphql": "^16.8.1", 32 | "graphql-request": "^6.1.0", 33 | "zod": "^3.22.4" 34 | }, 35 | "devDependencies": { 36 | "@jest/globals": "^29.7.0", 37 | "@types/jest": "^29.5.14", 38 | "@types/node": "^20.11.28", 39 | "jest": "^29.7.0", 40 | "ts-jest": "^29.1.2", 41 | "ts-node": "^10.9.2", 42 | "typescript": "^5.4.2" 43 | } 44 | } -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | concurrency: ${{ github.workflow }}-${{ github.ref }} 8 | 9 | jobs: 10 | release: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: write 14 | pull-requests: write 15 | id-token: write 16 | 17 | steps: 18 | - name: Checkout code 19 | uses: actions/checkout@v4 20 | with: 21 | fetch-depth: 0 22 | 23 | - name: Setup pnpm 24 | uses: pnpm/action-setup@v2 25 | with: 26 | version: 8 27 | 28 | - name: Setup Node.js 29 | uses: actions/setup-node@v4 30 | with: 31 | node-version: 20 32 | cache: 'pnpm' 33 | registry-url: 'https://registry.npmjs.org' 34 | 35 | - name: Install dependencies 36 | run: pnpm install --frozen-lockfile 37 | 38 | - name: Build packages 39 | run: pnpm build 40 | 41 | - name: Run tests 42 | run: pnpm test 43 | 44 | - name: Create Release Pull Request or Publish 45 | id: changesets 46 | uses: changesets/action@v1 47 | with: 48 | publish: pnpm changeset:publish 49 | title: "chore: release packages" 50 | commit: "chore: release packages" 51 | env: 52 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 53 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} -------------------------------------------------------------------------------- /packages/python-sdk/src/types.py: -------------------------------------------------------------------------------- 1 | 2 | from dataclasses import dataclass 3 | from enum import Enum 4 | from typing import List, Optional 5 | from decimal import Decimal 6 | 7 | @dataclass 8 | class Proposal: 9 | id: int 10 | price: int 11 | task_id: int 12 | agent: str 13 | 14 | @classmethod 15 | def from_dict(cls, data: dict) -> 'Proposal': 16 | return cls( 17 | id=int(data['id']), 18 | price=int(data['price']), 19 | task_id=int(data['taskId']), 20 | agent=data['agent'] 21 | ) 22 | 23 | class TaskType(Enum): 24 | SIMPLE = 0 25 | COMPLEX = 1 26 | COMPOSITE = 2 27 | 28 | class TaskStatus(Enum): 29 | CREATED = 0 30 | ASSIGNED = 1 31 | COMPLETED = 2 32 | FAILED = 3 33 | 34 | @dataclass 35 | class Skill: 36 | name: str 37 | level: int 38 | 39 | @dataclass 40 | class TaskData: 41 | id: str 42 | prompt: str 43 | task_type: TaskType 44 | assignee: Optional[str] 45 | status: TaskStatus 46 | owner: str 47 | 48 | @dataclass 49 | class AgentData: 50 | address: str 51 | model: str 52 | prompt: str 53 | skills: List[Skill] 54 | reputation: Decimal 55 | is_registered: bool 56 | 57 | @dataclass 58 | class NetworkConfig: 59 | chain_id: int 60 | name: Optional[str] 61 | rpc_url: str 62 | 63 | @dataclass 64 | class ContractConfig: 65 | task_registry_address: str 66 | agent_registry_address: str 67 | network: NetworkConfig 68 | -------------------------------------------------------------------------------- /packages/sdk/scripts/data/servicesList.ts: -------------------------------------------------------------------------------- 1 | import { Service } from "../../src/types"; 2 | 3 | export const servicesList: Service[] = [ 4 | { 5 | name: 'Bull-Post', 6 | category: 'Social', 7 | description: 'Bull-Post service will explain your project to the world!' 8 | }, 9 | { 10 | name: 'Reply', 11 | category: 'Social', 12 | description: 'Reply agents are great for interaction and possibly farm airdrops/whitelist spots!' 13 | }, 14 | { 15 | name: 'Campaign', 16 | category: 'Social', 17 | description: 'Agents will run a campaign on your behalf, ensuring attention and consistency' 18 | }, 19 | { 20 | name: 'Swap', 21 | category: 'Defi', 22 | description: 'Agent conducts a swap on your behalf using an optimal route with less fees' 23 | }, 24 | { 25 | name: 'Bridge', 26 | category: 'Defi', 27 | description: 'Agent conducts a swap on your behalf using an optimal route with less fees' 28 | }, 29 | { 30 | name: 'Provide LP', 31 | category: 'Defi', 32 | description: 'Agent conducts a swap on your behalf using an optimal route with less fees' 33 | }, 34 | { 35 | name: 'Markets', 36 | category: 'Research', 37 | description: 'Perfect for analyzing market data and providing accurate information' 38 | }, 39 | { 40 | name: 'Trends', 41 | category: 'Research', 42 | description: 'Get up-tp-date with the latest trends in the Crypto world!' 43 | }, 44 | { 45 | name: 'AI Agents', 46 | category: 'Research', 47 | description: 'Stay updated with the latest on AI Agents!' 48 | } 49 | ] -------------------------------------------------------------------------------- /packages/sdk/scripts/utils/setupSdk.ts: -------------------------------------------------------------------------------- 1 | import dotenv from "dotenv"; 2 | import { JsonRpcProvider, Wallet } from "ethers"; 3 | import { PinataSDK } from "pinata-web3"; 4 | import { Ensemble } from "../../src"; 5 | 6 | dotenv.config({ override: true }); 7 | 8 | 9 | const rpcUrl = process.env.RPC_URL!; 10 | const privateKey = process.env.PRIVATE_KEY!; 11 | const taskRegistryAddress = process.env.TASK_REGISTRY_ADDRESS!; 12 | const agentRegistryAddress = process.env.AGENTS_REGISTRY_ADDRESS!; 13 | const serviceRegistryAddress = process.env.SERVICE_REGISTRY_ADDRESS!; 14 | const pinataJwt = process.env.PINATA_JWT!; 15 | const chainId = parseInt(process.env.CHAIN_ID!, 10); 16 | const networkName = process.env.NETWORK_NAME!; 17 | 18 | export const setupEnv = () => { 19 | const provider = new JsonRpcProvider(process.env.RPC_URL!); 20 | const pk = process.env.PRIVATE_KEY!; 21 | const wallet = new Wallet(pk, provider); 22 | 23 | return { 24 | provider, 25 | signer: wallet, 26 | }; 27 | }; 28 | 29 | export const setupSdk = () => { 30 | const { signer } = setupEnv(); 31 | console.log("agentRegistryAddress", process.env.AGENTS_REGISTRY_ADDRESS!); 32 | 33 | const config = { 34 | network: { 35 | rpcUrl: rpcUrl, 36 | chainId: chainId, 37 | name: networkName, 38 | }, 39 | taskRegistryAddress: taskRegistryAddress, 40 | agentRegistryAddress: agentRegistryAddress, 41 | serviceRegistryAddress: serviceRegistryAddress, 42 | }; 43 | const sdk = Ensemble.create(config, signer, new PinataSDK({ pinataJwt })); 44 | return sdk; 45 | }; 46 | -------------------------------------------------------------------------------- /packages/subgraph/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | graph-node: 4 | image: graphprotocol/graph-node 5 | ports: 6 | - "8000:8000" 7 | - "8001:8001" 8 | - "8020:8020" 9 | - "8030:8030" 10 | - "8040:8040" 11 | depends_on: 12 | - ipfs 13 | - postgres 14 | extra_hosts: 15 | - host.docker.internal:host-gateway 16 | environment: 17 | postgres_host: postgres 18 | postgres_user: graph-node 19 | postgres_pass: let-me-in 20 | postgres_db: graph-node 21 | ipfs: "ipfs:5001" 22 | ethereum: "mainnet:http://host.docker.internal:8545" 23 | GRAPH_LOG: info 24 | ipfs: 25 | image: ipfs/kubo:v0.17.0 26 | ports: 27 | - "5001:5001" 28 | volumes: 29 | - ./data/ipfs:/data/ipfs 30 | postgres: 31 | image: postgres:14 32 | ports: 33 | - "5432:5432" 34 | command: 35 | [ 36 | "postgres", 37 | "-cshared_preload_libraries=pg_stat_statements", 38 | "-cmax_connections=200", 39 | ] 40 | environment: 41 | POSTGRES_USER: graph-node 42 | POSTGRES_PASSWORD: let-me-in 43 | POSTGRES_DB: graph-node 44 | # FIXME: remove this env. var. which we shouldn't need. Introduced by 45 | # , maybe as a 46 | # workaround for https://github.com/docker/for-mac/issues/6270? 47 | PGDATA: "/var/lib/postgresql/data" 48 | POSTGRES_INITDB_ARGS: "-E UTF8 --locale=C" 49 | volumes: 50 | - ./data/postgres:/var/lib/postgresql/data 51 | -------------------------------------------------------------------------------- /packages/sdk/scripts/data/proposalsList.ts: -------------------------------------------------------------------------------- 1 | import { AddProposalParams } from "../../src/types"; 2 | 3 | export const proposalsList: AddProposalParams[] = [ 4 | // ETH proposals (100x cheaper) 5 | { 6 | agentAddress: "0xc1ec8b9ca11ef907b959fed83272266b0e96b58d", 7 | serviceName: "Bull-Post", 8 | servicePrice: "1000000000000000", // 0.001 ETH 9 | tokenAddress: "0x0000000000000000000000000000000000000000" // ETH 10 | }, 11 | { 12 | agentAddress: "0xc1ec8b9ca11ef907b959fed83272266b0e96b58d", 13 | serviceName: "Reply", 14 | servicePrice: "500000000000000", // 0.0005 ETH 15 | tokenAddress: "0x0000000000000000000000000000000000000000" // ETH 16 | }, 17 | { 18 | agentAddress: "0xc1ec8b9ca11ef907b959fed83272266b0e96b58d", 19 | serviceName: "Campaign", 20 | servicePrice: "5000000000000000", // 0.005 ETH 21 | tokenAddress: "0x0000000000000000000000000000000000000000" // ETH 22 | }, 23 | { 24 | agentAddress: "0xc1ec8b9ca11ef907b959fed83272266b0e96b58d", 25 | serviceName: "Bull-Post", 26 | servicePrice: "100000", // 0.1 USDC 27 | tokenAddress: "0x036CbD53842c5426634e7929541eC2318f3dCF7e" // USDC on Base 28 | }, 29 | { 30 | agentAddress: "0xc1ec8b9ca11ef907b959fed83272266b0e96b58d", 31 | serviceName: "Reply", 32 | servicePrice: "50000", // 0.05 USDC 33 | tokenAddress: "0x036CbD53842c5426634e7929541eC2318f3dCF7e" // USDC on Base 34 | }, 35 | { 36 | agentAddress: "0xc1ec8b9ca11ef907b959fed83272266b0e96b58d", 37 | serviceName: "Campaign", 38 | servicePrice: "500000", // 0.5 USDC 39 | tokenAddress: "0x036CbD53842c5426634e7929541eC2318f3dCF7e" // USDC on Base 40 | } 41 | ]; -------------------------------------------------------------------------------- /packages/python-sdk/src/abi/TaskRegistry.abi.json: -------------------------------------------------------------------------------- 1 | 2 | [ 3 | { 4 | "inputs": [], 5 | "stateMutability": "nonpayable", 6 | "type": "constructor" 7 | }, 8 | { 9 | "anonymous": false, 10 | "inputs": [ 11 | { 12 | "indexed": true, 13 | "internalType": "uint256", 14 | "name": "taskId", 15 | "type": "uint256" 16 | }, 17 | { 18 | "indexed": false, 19 | "internalType": "string", 20 | "name": "result", 21 | "type": "string" 22 | } 23 | ], 24 | "name": "TaskCompleted", 25 | "type": "event" 26 | }, 27 | { 28 | "inputs": [ 29 | { 30 | "internalType": "string", 31 | "name": "prompt", 32 | "type": "string" 33 | }, 34 | { 35 | "internalType": "enum TaskRegistry.TaskType", 36 | "name": "taskType", 37 | "type": "uint8" 38 | } 39 | ], 40 | "name": "createTask", 41 | "outputs": [ 42 | { 43 | "internalType": "uint256", 44 | "name": "", 45 | "type": "uint256" 46 | } 47 | ], 48 | "stateMutability": "nonpayable", 49 | "type": "function" 50 | }, 51 | { 52 | "inputs": [ 53 | { 54 | "internalType": "uint256", 55 | "name": "taskId", 56 | "type": "uint256" 57 | } 58 | ], 59 | "name": "getStatus", 60 | "outputs": [ 61 | { 62 | "internalType": "enum TaskRegistry.TaskStatus", 63 | "name": "", 64 | "type": "uint8" 65 | } 66 | ], 67 | "stateMutability": "view", 68 | "type": "function" 69 | } 70 | ] 71 | -------------------------------------------------------------------------------- /.taskmaster/templates/example_prd.txt: -------------------------------------------------------------------------------- 1 | 2 | # Overview 3 | [Provide a high-level overview of your product here. Explain what problem it solves, who it's for, and why it's valuable.] 4 | 5 | # Core Features 6 | [List and describe the main features of your product. For each feature, include: 7 | - What it does 8 | - Why it's important 9 | - How it works at a high level] 10 | 11 | # User Experience 12 | [Describe the user journey and experience. Include: 13 | - User personas 14 | - Key user flows 15 | - UI/UX considerations] 16 | 17 | 18 | # Technical Architecture 19 | [Outline the technical implementation details: 20 | - System components 21 | - Data models 22 | - APIs and integrations 23 | - Infrastructure requirements] 24 | 25 | # Development Roadmap 26 | [Break down the development process into phases: 27 | - MVP requirements 28 | - Future enhancements 29 | - Do not think about timelines whatsoever -- all that matters is scope and detailing exactly what needs to be build in each phase so it can later be cut up into tasks] 30 | 31 | # Logical Dependency Chain 32 | [Define the logical order of development: 33 | - Which features need to be built first (foundation) 34 | - Getting as quickly as possible to something usable/visible front end that works 35 | - Properly pacing and scoping each feature so it is atomic but can also be built upon and improved as development approaches] 36 | 37 | # Risks and Mitigations 38 | [Identify potential risks and how they'll be addressed: 39 | - Technical challenges 40 | - Figuring out the MVP that we can build upon 41 | - Resource constraints] 42 | 43 | # Appendix 44 | [Include any additional information: 45 | - Research findings 46 | - Technical specifications] 47 | -------------------------------------------------------------------------------- /packages/contracts/scripts/create-task.ts: -------------------------------------------------------------------------------- 1 | import { TaskRegistryUpgradeable } from "../typechain-types"; 2 | require('dotenv').config('../.env'); 3 | 4 | const hre = require("hardhat"); 5 | 6 | async function main() { 7 | const taskRegistryAddress = process.env.TASK_REGISTRY_ADDRESS; 8 | console.log("Task Registry Address:", taskRegistryAddress); 9 | // Get the contract factory and deployer 10 | const [deployer] = await hre.ethers.getSigners(); 11 | 12 | console.log("account address:", deployer.address); 13 | 14 | // Get the deployed contract instance 15 | const TaskRegistry = await hre.ethers.getContractFactory("TaskRegistryUpgradeable"); 16 | const taskRegistry = await TaskRegistry.attach(taskRegistryAddress) as TaskRegistryUpgradeable; 17 | 18 | try { 19 | // Create a task without ignition 20 | const simpleTask = await taskRegistry.createTask( 21 | "Do X for me", 22 | 0 23 | ); 24 | // Wait for the transaction to be mined 25 | const simpleTaskReceipt = await simpleTask.wait(); 26 | console.log("Simple task created in tx:", simpleTaskReceipt?.hash); 27 | 28 | 29 | // const secondTask = await taskRegistry.createTask( 30 | // "Do Y for me", 31 | // 0 32 | // ); 33 | // Wait for the transaction to be mined 34 | // const secondTaskReceipt = await secondTask.wait(); 35 | // console.log("Simple task created in tx:", secondTaskReceipt.hash); 36 | 37 | } catch (error) { 38 | console.error("Error:", error); 39 | } 40 | } 41 | 42 | main() 43 | .then(() => process.exit(0)) 44 | .catch((error) => { 45 | console.error(error); 46 | process.exit(1); 47 | }); -------------------------------------------------------------------------------- /packages/contracts/ignition/modules/TaskRegistryUpgradeable.ts: -------------------------------------------------------------------------------- 1 | import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; 2 | import AgentsRegistryUpgradeableModule from "./AgentsRegistryUpgradeable"; 3 | 4 | const TaskRegistryUpgradeableModule = buildModule("TaskRegistryUpgradeableModule", (m) => { 5 | // Use the AgentsRegistryUpgradeable module (which includes ServiceRegistry) 6 | const { agentsRegistry, serviceRegistry } = m.useModule(AgentsRegistryUpgradeableModule); 7 | 8 | // Deploy the TaskRegistryUpgradeable implementation 9 | const taskRegistryImpl = m.contract("TaskRegistryUpgradeable", [], { 10 | id: "TaskRegistryImpl" 11 | }); 12 | 13 | // Get initialization parameters 14 | const initialTaskId = m.getParameter("initialTaskId", 1); 15 | 16 | // Encode the initialize function call with required parameters 17 | const initializeData = m.encodeFunctionCall(taskRegistryImpl, "initialize", [ 18 | initialTaskId, 19 | agentsRegistry 20 | ]); 21 | 22 | // Deploy the UUPS proxy with the implementation and initialization data 23 | const taskRegistryProxy = m.contract("ERC1967Proxy", [ 24 | taskRegistryImpl, 25 | initializeData, 26 | ], { 27 | id: "TaskRegistryProxy" 28 | }); 29 | 30 | // Create a contract instance that uses the proxy address but the implementation ABI 31 | const taskRegistry = m.contractAt("TaskRegistryUpgradeable", taskRegistryProxy, { 32 | id: "TaskRegistryProxied" 33 | }); 34 | 35 | return { 36 | taskRegistry, 37 | taskRegistryProxy, 38 | taskRegistryImpl, 39 | agentsRegistry, 40 | serviceRegistry 41 | }; 42 | }); 43 | 44 | export default TaskRegistryUpgradeableModule; -------------------------------------------------------------------------------- /packages/contracts/contracts/mocks/MockERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.20; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 5 | 6 | /** 7 | * @title MockERC20 8 | * @dev A simple ERC20 token implementation for testing purposes 9 | */ 10 | contract MockERC20 is ERC20 { 11 | uint8 private _decimals; 12 | 13 | /** 14 | * @dev Constructor with custom decimals 15 | * @param name The name of the token 16 | * @param symbol The symbol of the token 17 | * @param initialSupply The initial supply of tokens to mint 18 | * @param decimalsValue The number of decimals for the token 19 | */ 20 | constructor( 21 | string memory name, 22 | string memory symbol, 23 | uint256 initialSupply, 24 | uint8 decimalsValue 25 | ) ERC20(name, symbol) { 26 | _decimals = decimalsValue; 27 | _mint(msg.sender, initialSupply); 28 | } 29 | 30 | /** 31 | * @dev Returns the number of decimals used to get its user representation 32 | */ 33 | function decimals() public view virtual override returns (uint8) { 34 | return _decimals; 35 | } 36 | 37 | /** 38 | * @dev Mint tokens to a specific address (for testing purposes) 39 | * @param to The address to mint tokens to 40 | * @param amount The amount of tokens to mint 41 | */ 42 | function mint(address to, uint256 amount) external { 43 | _mint(to, amount); 44 | } 45 | 46 | /** 47 | * @dev Burn tokens from a specific address (for testing purposes) 48 | * @param from The address to burn tokens from 49 | * @param amount The amount of tokens to burn 50 | */ 51 | function burn(address from, uint256 amount) external { 52 | _burn(from, amount); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /packages/subgraph/src/task-registry.ts: -------------------------------------------------------------------------------- 1 | import { BigInt, Bytes } from "@graphprotocol/graph-ts"; 2 | import { 3 | TaskCreated, 4 | TaskStatusChanged, 5 | TaskCompleted, 6 | TaskRated 7 | } from "../generated/TaskRegistry/TaskRegistry" 8 | import { 9 | Task, 10 | } from "../generated/schema" 11 | import { blacklistedAgents } from "./constants"; 12 | 13 | export function handleTaskCreated(event: TaskCreated): void { 14 | let entity = new Task(event.params.taskId.toString()); 15 | 16 | let assignee = event.params.assignee.toHexString(); 17 | 18 | if (blacklistedAgents.includes(assignee)) { 19 | return 20 | } 21 | 22 | entity.taskId = event.params.taskId.toI32(); 23 | 24 | entity.prompt = event.params.prompt; 25 | entity.issuer = event.params.issuer; 26 | entity.proposalId = event.params.proposalId; 27 | entity.assignee = assignee; 28 | entity.status = '1'; 29 | entity.rating = BigInt.fromI32(0); 30 | 31 | entity.save(); 32 | } 33 | 34 | export function handleTaskStatusChanged(event: TaskStatusChanged): void { 35 | let entity = Task.load(event.params.taskId.toString()); 36 | if (entity == null) { 37 | return 38 | } 39 | 40 | entity.status = event.params.status.toString(); 41 | 42 | entity.save(); 43 | } 44 | 45 | export function handleTaskStatusCompleted(event: TaskCompleted): void { 46 | let entity = Task.load(event.params.taskId.toString()); 47 | if (entity == null) { 48 | return 49 | } 50 | 51 | entity.result = event.params.result; 52 | 53 | entity.save(); 54 | } 55 | 56 | export function handleTaskRated(event: TaskRated): void { 57 | let entity = Task.load(event.params.taskId.toString()); 58 | if (entity == null) { 59 | return 60 | } 61 | 62 | entity.rating = BigInt.fromI32(event.params.rating); 63 | 64 | entity.save(); 65 | } 66 | 67 | -------------------------------------------------------------------------------- /packages/sdk/test/optionalSigner.basic.test.ts: -------------------------------------------------------------------------------- 1 | import { Ensemble } from "../src/ensemble"; 2 | import { ethers } from "ethers"; 3 | 4 | // Basic integration test for optional signer functionality 5 | describe("Optional Signer Basic Integration", () => { 6 | const mockConfig = { 7 | serviceRegistryAddress: "0x123", 8 | agentRegistryAddress: "0x456", 9 | taskRegistryAddress: "0x789", 10 | network: { 11 | chainId: 1, 12 | name: "mainnet", 13 | rpcUrl: "https://test-rpc.com" 14 | }, 15 | subgraphUrl: "https://test-subgraph.com" 16 | }; 17 | 18 | it("should create SDK without signer", () => { 19 | const ensemble = Ensemble.create(mockConfig); 20 | expect(ensemble).toBeInstanceOf(Ensemble); 21 | }); 22 | 23 | it("should create SDK with signer", () => { 24 | const mockSigner = {} as ethers.Signer; 25 | const ensemble = Ensemble.create(mockConfig, mockSigner); 26 | expect(ensemble).toBeInstanceOf(Ensemble); 27 | }); 28 | 29 | it("should allow setting signer after creation", () => { 30 | const ensemble = Ensemble.create(mockConfig); 31 | const mockSigner = {} as ethers.Signer; 32 | 33 | expect(() => ensemble.setSigner(mockSigner)).not.toThrow(); 34 | }); 35 | 36 | it("should throw error for write operation without signer", async () => { 37 | const ensemble = Ensemble.create(mockConfig); 38 | 39 | await expect(ensemble.createTask({ 40 | prompt: "test", 41 | proposalId: "1" 42 | })).rejects.toThrow("Signer required for write operations. Call setSigner() first."); 43 | }); 44 | 45 | it("should throw error for getWalletAddress without signer", async () => { 46 | const ensemble = Ensemble.create(mockConfig); 47 | 48 | await expect(ensemble.getWalletAddress()).rejects.toThrow( 49 | "Signer required for write operations. Call setSigner() first." 50 | ); 51 | }); 52 | }); -------------------------------------------------------------------------------- /packages/contracts/ignition/modules/AgentsRegistryUpgradeable.ts: -------------------------------------------------------------------------------- 1 | import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; 2 | import ServiceRegistryUpgradeableModule from "./ServiceRegistryUpgradeable"; 3 | 4 | const AgentsRegistryUpgradeableModule = buildModule("AgentsRegistryUpgradeableModule", (m) => { 5 | // Use the ServiceRegistryUpgradeable module 6 | const { serviceRegistry } = m.useModule(ServiceRegistryUpgradeableModule); 7 | 8 | // Deploy the AgentsRegistryUpgradeable implementation 9 | const agentsRegistryImpl = m.contract("AgentsRegistryUpgradeable", [], { 10 | id: "AgentsRegistryImpl" 11 | }); 12 | 13 | // For initialization, we need the V1 registry address and service registry 14 | // Using a placeholder address for V1 registry (can be updated via parameters) 15 | const v1RegistryAddress = m.getParameter("v1RegistryAddress", "0x0000000000000000000000000000000000000000"); 16 | 17 | // Encode the initialize function call with required parameters 18 | const initializeData = m.encodeFunctionCall(agentsRegistryImpl, "initialize", [ 19 | v1RegistryAddress, 20 | serviceRegistry 21 | ]); 22 | 23 | // Deploy the UUPS proxy with the implementation and initialization data 24 | const agentsRegistryProxy = m.contract("ERC1967Proxy", [ 25 | agentsRegistryImpl, 26 | initializeData, 27 | ], { 28 | id: "AgentsRegistryProxy" 29 | }); 30 | 31 | // Create a contract instance that uses the proxy address but the implementation ABI 32 | const agentsRegistry = m.contractAt("AgentsRegistryUpgradeable", agentsRegistryProxy, { 33 | id: "AgentsRegistryProxied" 34 | }); 35 | 36 | return { 37 | agentsRegistry, 38 | agentsRegistryProxy, 39 | agentsRegistryImpl, 40 | serviceRegistry 41 | }; 42 | }); 43 | 44 | export default AgentsRegistryUpgradeableModule; -------------------------------------------------------------------------------- /packages/contracts/memory-bank/projectbrief.md: -------------------------------------------------------------------------------- 1 | # Project Brief: Ensemble Framework Smart Contracts 2 | 3 | ## Project Overview 4 | The Ensemble Framework Smart Contracts project provides the foundational on-chain infrastructure for an AI agent economy. This system enables AI agents to register, discover services, create and execute tasks, and interact within a decentralized ecosystem. 5 | 6 | ## Core Objective 7 | Build a comprehensive smart contract ecosystem that facilitates: 8 | - AI agent registration and identity management 9 | - Decentralized task creation and execution 10 | - Service discovery and registry 11 | - Economic interactions between AI agents 12 | 13 | ## Key Components 14 | 1. **Agent Registry** - Central registry for AI agent identities and capabilities 15 | 2. **Task Registry** - System for creating, managing, and executing tasks 16 | 3. **Service Registry** - Discovery mechanism for available services 17 | 4. **Economic Framework** - Token-based economy for agent interactions 18 | 19 | ## Technical Requirements 20 | - Solidity 0.8.20+ with optimization enabled 21 | - OpenZeppelin contracts for security and standards 22 | - Multi-network deployment (Base, Base Sepolia, NeonEVM) 23 | - Gas-optimized implementations 24 | - Comprehensive test coverage 25 | - Modular and upgradeable architecture 26 | 27 | ## Success Criteria 28 | - Secure and auditable smart contracts 29 | - Efficient gas usage across all operations 30 | - Seamless multi-network deployment 31 | - Comprehensive API for external integrations 32 | - High test coverage and robust error handling 33 | 34 | ## Deployment Networks 35 | - **Development**: Local Hardhat network 36 | - **Testing**: Base Sepolia testnet 37 | - **Future**: Additional EVM-compatible chains 38 | 39 | ## Current Status 40 | The project has v3 as the current stable release on Base Sepolia, with v2 now deprecated. Core registries are operational and ready for production use. -------------------------------------------------------------------------------- /packages/subgraph/tests/agents-registry.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | assert, 3 | describe, 4 | test, 5 | clearStore, 6 | beforeAll, 7 | afterAll 8 | } from "matchstick-as/assembly/index" 9 | import { Address, BigInt } from "@graphprotocol/graph-ts" 10 | import { AgentRegistered } from "../generated/schema" 11 | import { AgentRegistered as AgentRegisteredEvent } from "../generated/AgentsRegistry/AgentsRegistry" 12 | import { handleAgentRegistered } from "../src/agents-registry" 13 | import { createAgentRegisteredEvent } from "./agents-registry-utils" 14 | 15 | // Tests structure (matchstick-as >=0.5.0) 16 | // https://thegraph.com/docs/en/developer/matchstick/#tests-structure-0-5-0 17 | 18 | describe("Describe entity assertions", () => { 19 | beforeAll(() => { 20 | let agent = Address.fromString("0x0000000000000000000000000000000000000001") 21 | let model = "Example string value" 22 | let newAgentRegisteredEvent = createAgentRegisteredEvent(agent, model) 23 | handleAgentRegistered(newAgentRegisteredEvent) 24 | }) 25 | 26 | afterAll(() => { 27 | clearStore() 28 | }) 29 | 30 | // For more test scenarios, see: 31 | // https://thegraph.com/docs/en/developer/matchstick/#write-a-unit-test 32 | 33 | test("AgentRegistered created and stored", () => { 34 | assert.entityCount("AgentRegistered", 1) 35 | 36 | // 0xa16081f360e3847006db660bae1c6d1b2e17ec2a is the default address used in newMockEvent() function 37 | assert.fieldEquals( 38 | "AgentRegistered", 39 | "0xa16081f360e3847006db660bae1c6d1b2e17ec2a-1", 40 | "agent", 41 | "0x0000000000000000000000000000000000000001" 42 | ) 43 | assert.fieldEquals( 44 | "AgentRegistered", 45 | "0xa16081f360e3847006db660bae1c6d1b2e17ec2a-1", 46 | "model", 47 | "Example string value" 48 | ) 49 | 50 | // More assert options: 51 | // https://thegraph.com/docs/en/developer/matchstick/#asserts 52 | }) 53 | }) 54 | -------------------------------------------------------------------------------- /.github/workflows/prerelease.yml: -------------------------------------------------------------------------------- 1 | name: Prerelease 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | tag: 7 | description: 'Prerelease tag' 8 | required: true 9 | type: choice 10 | options: 11 | - alpha 12 | - beta 13 | - rc 14 | 15 | jobs: 16 | prerelease: 17 | runs-on: ubuntu-latest 18 | permissions: 19 | contents: write 20 | id-token: write 21 | 22 | steps: 23 | - name: Checkout code 24 | uses: actions/checkout@v4 25 | with: 26 | fetch-depth: 0 27 | 28 | - name: Setup pnpm 29 | uses: pnpm/action-setup@v2 30 | with: 31 | version: 8 32 | 33 | - name: Setup Node.js 34 | uses: actions/setup-node@v4 35 | with: 36 | node-version: 20 37 | cache: 'pnpm' 38 | registry-url: 'https://registry.npmjs.org' 39 | 40 | - name: Install dependencies 41 | run: pnpm install --frozen-lockfile 42 | 43 | - name: Build packages 44 | run: pnpm build 45 | 46 | - name: Run tests 47 | run: pnpm test 48 | 49 | - name: Enter prerelease mode 50 | run: pnpm changeset pre enter ${{ github.event.inputs.tag }} 51 | 52 | - name: Version packages 53 | run: pnpm changeset:version 54 | 55 | - name: Commit prerelease changes 56 | run: | 57 | git config --local user.email "action@github.com" 58 | git config --local user.name "GitHub Action" 59 | git add . 60 | git commit -m "chore: enter ${{ github.event.inputs.tag }} prerelease mode" || exit 0 61 | 62 | - name: Publish prerelease 63 | run: pnpm changeset:publish --tag ${{ github.event.inputs.tag }} 64 | env: 65 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 66 | 67 | - name: Push changes 68 | run: git push --follow-tags -------------------------------------------------------------------------------- /packages/cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ensemble-ai/cli", 3 | "version": "0.1.1", 4 | "description": "Command-line interface for Ensemble agent management", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "bin": { 8 | "ensemble": "dist/bin/ensemble.js" 9 | }, 10 | "files": [ 11 | "dist", 12 | "bin" 13 | ], 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/ensemble-codes/ensemble-framework.git", 17 | "directory": "packages/cli" 18 | }, 19 | "publishConfig": { 20 | "access": "public", 21 | "registry": "https://registry.npmjs.org/" 22 | }, 23 | "license": "MIT", 24 | "homepage": "https://github.com/ensemble-codes/ensemble-framework/tree/main/packages/cli", 25 | "bugs": { 26 | "url": "https://github.com/ensemble-codes/ensemble-framework/issues" 27 | }, 28 | "scripts": { 29 | "clean": "rm -rf dist", 30 | "build": "tsc", 31 | "watch": "tsc --watch", 32 | "test": "jest --verbose", 33 | "typecheck": "tsc --noEmit", 34 | "dev": "tsx src/bin/ensemble.ts", 35 | "prepublishOnly": "npm run build" 36 | }, 37 | "dependencies": { 38 | "@ensemble-ai/sdk": "workspace:*", 39 | "commander": "^12.0.0", 40 | "yaml": "^2.4.0", 41 | "chalk": "^5.3.0", 42 | "inquirer": "^10.0.0", 43 | "ora": "^8.0.0", 44 | "table": "^6.8.0", 45 | "dotenv": "^16.4.7", 46 | "ethers": "^6.9.0", 47 | "bip39": "^3.1.0", 48 | "crypto-js": "^4.2.0", 49 | "pinata-web3": "^0.5.4" 50 | }, 51 | "devDependencies": { 52 | "@types/inquirer": "^9.0.7", 53 | "@types/jest": "^29.5.14", 54 | "@types/node": "^20.10.0", 55 | "@types/crypto-js": "^4.2.0", 56 | "jest": "^29.7.0", 57 | "ts-jest": "^29.2.5", 58 | "tsx": "^4.7.0", 59 | "typescript": "^5.3.2" 60 | }, 61 | "keywords": [ 62 | "ensemble", 63 | "cli", 64 | "agent", 65 | "blockchain", 66 | "web3" 67 | ], 68 | "author": "Ensemble AI" 69 | } -------------------------------------------------------------------------------- /packages/sdk/.gitignore: -------------------------------------------------------------------------------- 1 | # Dist 2 | dist 3 | 4 | # Node.js 5 | node_modules/ 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # TypeScript 11 | *.tsbuildinfo 12 | typechain/ 13 | 14 | # Logs 15 | logs 16 | *.log 17 | logs/* 18 | 19 | # Dependency directories 20 | jspm_packages/ 21 | 22 | # Optional npm cache directory 23 | .npm 24 | 25 | # Optional eslint cache 26 | .eslintcache 27 | 28 | # Optional REPL history 29 | .node_repl_history 30 | 31 | # Output of 'npm pack' 32 | *.tgz 33 | 34 | # dotenv environment variables file 35 | .env 36 | *.env 37 | # Parcel cache 38 | .cache 39 | 40 | # Next.js build output 41 | .next 42 | 43 | # Nuxt.js build output 44 | .nuxt 45 | 46 | # Gatsby files 47 | .cache/ 48 | public/ 49 | 50 | # Vuepress build output 51 | .vuepress/dist 52 | 53 | # Serverless directories 54 | .serverless/ 55 | 56 | # FuseBox cache 57 | .fusebox/ 58 | 59 | # DynamoDB Local files 60 | .dynamodb/ 61 | 62 | # TernJS port file 63 | .tern-port 64 | 65 | # TypeScript output 66 | dist/ 67 | build/ 68 | 69 | # Storybook files 70 | .out/ 71 | .storybook-out/ 72 | 73 | # Static site generators 74 | out/ 75 | 76 | # SvelteKit build output 77 | .svelte-kit/ 78 | 79 | # Temporary folders 80 | tmp/ 81 | temp/ 82 | 83 | # Vite build output 84 | .vite/ 85 | 86 | # Remix build output 87 | .build/ 88 | 89 | # Capacitor config 90 | capacitor.config.ts 91 | 92 | # Expo files 93 | .expo/ 94 | .expo-shared/ 95 | 96 | # VS Code 97 | .vscode/ 98 | 99 | # JetBrains IDEs 100 | .idea/ 101 | 102 | # MacOS 103 | .DS_Store 104 | 105 | # Windows 106 | Thumbs.db 107 | 108 | # Graphclient 109 | .graphclient/ 110 | 111 | dev-debug.log 112 | # Environment variables 113 | # Editor directories and files 114 | .idea 115 | .vscode 116 | *.suo 117 | *.ntvs* 118 | *.njsproj 119 | *.sln 120 | *.sw? 121 | # OS specific 122 | 123 | # Task files 124 | # tasks.json 125 | # tasks/ 126 | -------------------------------------------------------------------------------- /packages/contracts/contracts/lib/TransferHelper.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | 4 | // helper methods for interacting with ERC20 tokens and sending ETH that do not consistently return true/false 5 | library TransferHelper { 6 | function safeApprove( 7 | address token, 8 | address to, 9 | uint256 value 10 | ) internal { 11 | // bytes4(keccak256(bytes('approve(address,uint256)'))); 12 | (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x095ea7b3, to, value)); 13 | require( 14 | success && (data.length == 0 || abi.decode(data, (bool))), 15 | 'TransferHelper::safeApprove: approve failed' 16 | ); 17 | } 18 | 19 | function safeTransfer( 20 | address token, 21 | address to, 22 | uint256 value 23 | ) internal { 24 | // bytes4(keccak256(bytes('transfer(address,uint256)'))); 25 | (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0xa9059cbb, to, value)); 26 | require( 27 | success && (data.length == 0 || abi.decode(data, (bool))), 28 | 'TransferHelper::safeTransfer: transfer failed' 29 | ); 30 | } 31 | 32 | function safeTransferFrom( 33 | address token, 34 | address from, 35 | address to, 36 | uint256 value 37 | ) internal { 38 | // bytes4(keccak256(bytes('transferFrom(address,address,uint256)'))); 39 | (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x23b872dd, from, to, value)); 40 | require( 41 | success && (data.length == 0 || abi.decode(data, (bool))), 42 | 'TransferHelper::transferFrom: transferFrom failed' 43 | ); 44 | } 45 | 46 | function safeTransferETH(address to, uint256 value) internal { 47 | (bool success, ) = to.call{value: value}(new bytes(0)); 48 | require(success, 'TransferHelper::safeTransferETH: ETH transfer failed'); 49 | } 50 | } -------------------------------------------------------------------------------- /packages/contracts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ensemble-ai/contracts", 3 | "version": "0.3.2", 4 | "description": "Smart contracts for the Ensemble Framework", 5 | "main": "hardhat.config.js", 6 | "files": [ 7 | "contracts", 8 | "artifacts", 9 | "typechain-types" 10 | ], 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/ensemble-codes/ensemble-framework.git", 14 | "directory": "packages/contracts" 15 | }, 16 | "publishConfig": { 17 | "access": "public", 18 | "registry": "https://registry.npmjs.org/" 19 | }, 20 | "license": "MIT", 21 | "homepage": "https://github.com/ensemble-codes/ensemble-framework/tree/main/packages/contracts", 22 | "bugs": { 23 | "url": "https://github.com/ensemble-codes/ensemble-framework/issues" 24 | }, 25 | "directories": { 26 | "test": "test" 27 | }, 28 | "scripts": { 29 | "compile": "hardhat compile", 30 | "clean": "hardhat clean", 31 | "test": "hardhat test", 32 | "test:coverage": "hardhat coverage", 33 | "test:gas": "REPORT_GAS=true hardhat test", 34 | "node": "hardhat node", 35 | "deploy:local": "npx hardhat ignition deploy ignition/modules/DeployAll.ts --network local", 36 | "deploy:testnet": "npx hardhat ignition deploy ignition/modules/DeployAll.ts --network baseSepolia", 37 | "verify:testnet": "npx hardhat ignition verify chain-84532" 38 | 39 | }, 40 | "keywords": [], 41 | "author": "Ensemble Framework Team", 42 | "dependencies": { 43 | "@nomicfoundation/hardhat-ignition": "0.15.8", 44 | "@nomicfoundation/hardhat-ignition-ethers": "0.15.8", 45 | "@nomicfoundation/hardhat-toolbox": "^5.0.0", 46 | "@openzeppelin/contracts": "^5.1.0", 47 | "@openzeppelin/contracts-upgradeable": "5.3.0", 48 | "chai": "4.5.0", 49 | "dotenv": "16.4.5", 50 | "ensemble-sdk": "file:../sdk", 51 | "hardhat": "^2.22.18", 52 | "solidity-coverage": "0.8.14" 53 | }, 54 | "devDependencies": { 55 | "@openzeppelin/hardhat-upgrades": "3.9.0" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /packages/cli/src/commands/agents/index.ts: -------------------------------------------------------------------------------- 1 | import { Command } from 'commander'; 2 | import chalk from 'chalk'; 3 | import { getAgentsCommand } from './get'; 4 | import { listAgentsCommand } from './list'; 5 | import { registerAgentCommand } from './register'; 6 | import { updateAgentCommand } from './update'; 7 | 8 | export const agentsCommand = new Command('agents') 9 | .description('Agent management commands') 10 | .argument('[address]', 'Agent address (optional - if provided, fetches specific agent)') 11 | .option('-h, --help', 'Display help information') 12 | 13 | // Sub-commands 14 | agentsCommand.addCommand(getAgentsCommand); 15 | agentsCommand.addCommand(listAgentsCommand); 16 | agentsCommand.addCommand(registerAgentCommand); 17 | agentsCommand.addCommand(updateAgentCommand); 18 | 19 | // Handle direct agent address or show help 20 | agentsCommand.action(async (address?: string, options?: any) => { 21 | if (options?.help) { 22 | agentsCommand.outputHelp(); 23 | return; 24 | } 25 | 26 | if (address) { 27 | // If an address is provided, fetch that specific agent 28 | try { 29 | const { createSDKInstance } = await import('../../utils/sdk'); 30 | const { formatOutput } = await import('../../utils/formatters'); 31 | 32 | const sdk = await createSDKInstance(); 33 | const agentService = sdk.agents; 34 | 35 | console.log(chalk.blue(`🔍 Fetching agent ${address}...`)); 36 | 37 | const agent = await agentService.getAgentRecord(address); 38 | 39 | console.log(chalk.green('✅ Agent found')); 40 | 41 | const output = formatOutput([agent], 'yaml', true); 42 | console.log(output); 43 | 44 | } catch (error: any) { 45 | console.error(chalk.red('❌ Error fetching agent:')); 46 | console.error(chalk.red(error.message)); 47 | process.exit(1); 48 | } 49 | } else { 50 | // No address provided, show help 51 | console.log(chalk.yellow('Please specify an agent address or use a subcommand.')); 52 | agentsCommand.outputHelp(); 53 | } 54 | }); -------------------------------------------------------------------------------- /packages/contracts/scripts/deploy.js: -------------------------------------------------------------------------------- 1 | import { ethers, upgrades } from "hardhat"; 2 | 3 | async function main() { 4 | const [deployer] = await ethers.getSigners(); 5 | console.log("Deploying contracts with account:", deployer.address); 6 | 7 | // Deploy ServiceRegistryUpgradeable 8 | const ServiceRegistry = await ethers.getContractFactory("ServiceRegistryUpgradeable"); 9 | const serviceRegistry = await upgrades.deployProxy(ServiceRegistry, [], { 10 | initializer: "initialize", 11 | kind: "uups" 12 | }); 13 | await serviceRegistry.waitForDeployment(); 14 | console.log("ServiceRegistryUpgradeable deployed to:", await serviceRegistry.getAddress()); 15 | 16 | // Deploy mock V1 registry for compatibility 17 | const mockV1Registry = await upgrades.deployProxy(ServiceRegistry, [], { 18 | initializer: "initialize", 19 | kind: "uups" 20 | }); 21 | await mockV1Registry.waitForDeployment(); 22 | 23 | // Deploy AgentsRegistryUpgradeable 24 | const AgentsRegistry = await ethers.getContractFactory("AgentsRegistryUpgradeable"); 25 | const agentsRegistry = await upgrades.deployProxy(AgentsRegistry, [await mockV1Registry.getAddress(), await serviceRegistry.getAddress()], { 26 | initializer: "initialize", 27 | kind: "uups" 28 | }); 29 | await agentsRegistry.waitForDeployment(); 30 | console.log("AgentsRegistryUpgradeable deployed to:", await agentsRegistry.getAddress()); 31 | 32 | // Deploy TaskRegistryUpgradeable 33 | const TaskRegistry = await ethers.getContractFactory("TaskRegistryUpgradeable"); 34 | const taskRegistry = await upgrades.deployProxy(TaskRegistry, [1, await agentsRegistry.getAddress()], { 35 | initializer: "initialize", 36 | kind: "uups" 37 | }); 38 | await taskRegistry.waitForDeployment(); 39 | console.log("TaskRegistryUpgradeable deployed to:", await taskRegistry.getAddress()); 40 | } 41 | 42 | main() 43 | .then(() => process.exit(0)) 44 | .catch((error) => { 45 | console.error(error); 46 | process.exit(1); 47 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ensemble-framework", 3 | "private": true, 4 | "description": "Monorepo for Ensemble Framework packages", 5 | "workspaces": [ 6 | "packages/*" 7 | ], 8 | "scripts": { 9 | "build": "pnpm -r build", 10 | "build:sdk": "pnpm --filter @ensemble-ai/sdk build", 11 | "build:cli": "pnpm --filter @ensemble-ai/cli build", 12 | "build:contracts": "pnpm --filter @ensemble-ai/contracts build", 13 | "test": "pnpm -r --filter='!@ensemble-ai/subgraph' --filter='!@ensemble-ai/mcp-server' --filter='!@ensemble-ai/cli' test", 14 | "test:sdk": "pnpm --filter @ensemble-ai/sdk test", 15 | "test:cli": "pnpm --filter @ensemble-ai/cli test", 16 | "test:subgraph": "pnpm --filter @ensemble-ai/subgraph test", 17 | "test:mcp": "pnpm --filter @ensemble-ai/mcp-server test", 18 | "typecheck": "pnpm -r typecheck", 19 | "changeset": "changeset", 20 | "changeset:status": "changeset status --verbose", 21 | "changeset:version": "changeset version && pnpm install --no-frozen-lockfile", 22 | "changeset:publish": "pnpm build && changeset publish", 23 | "changeset:publish:alpha": "pnpm build && changeset publish --tag alpha", 24 | "changeset:publish:beta": "pnpm build && changeset publish --tag beta", 25 | "release": "pnpm run changeset:version && pnpm run changeset:publish", 26 | "release:dry": "pnpm build && changeset publish --dry-run", 27 | "clean": "pnpm -r --parallel exec rm -rf dist node_modules", 28 | "format": "prettier --write \"packages/**/*.{ts,tsx,js,jsx,json,md}\"", 29 | "lint": "eslint packages/*/src --ext .ts,.tsx" 30 | }, 31 | "devDependencies": { 32 | "@changesets/cli": "^2.27.1", 33 | "@typescript-eslint/eslint-plugin": "^6.0.0", 34 | "@typescript-eslint/parser": "^6.0.0", 35 | "eslint": "^8.0.0", 36 | "prettier": "^3.0.0", 37 | "typescript": "^5.3.2" 38 | }, 39 | "engines": { 40 | "node": ">=20.18.1", 41 | "pnpm": ">=8.0.0" 42 | }, 43 | "packageManager": "pnpm@8.15.0", 44 | "repository": { 45 | "type": "git", 46 | "url": "https://github.com/ensemble-codes/ensemble-framework.git" 47 | }, 48 | "license": "MIT" 49 | } -------------------------------------------------------------------------------- /packages/subgraph/tests/task-registry.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | assert, 3 | describe, 4 | test, 5 | clearStore, 6 | beforeAll, 7 | afterAll 8 | } from "matchstick-as/assembly/index" 9 | import { Address, BigInt } from "@graphprotocol/graph-ts" 10 | import { OwnershipTransferred } from "../generated/schema" 11 | import { OwnershipTransferred as OwnershipTransferredEvent } from "../generated/TaskRegistry/TaskRegistry" 12 | import { handleOwnershipTransferred } from "../src/task-registry" 13 | import { createOwnershipTransferredEvent } from "./task-registry-utils" 14 | 15 | // Tests structure (matchstick-as >=0.5.0) 16 | // https://thegraph.com/docs/en/developer/matchstick/#tests-structure-0-5-0 17 | 18 | describe("Describe entity assertions", () => { 19 | beforeAll(() => { 20 | let previousOwner = Address.fromString( 21 | "0x0000000000000000000000000000000000000001" 22 | ) 23 | let newOwner = Address.fromString( 24 | "0x0000000000000000000000000000000000000001" 25 | ) 26 | let newOwnershipTransferredEvent = createOwnershipTransferredEvent( 27 | previousOwner, 28 | newOwner 29 | ) 30 | handleOwnershipTransferred(newOwnershipTransferredEvent) 31 | }) 32 | 33 | afterAll(() => { 34 | clearStore() 35 | }) 36 | 37 | // For more test scenarios, see: 38 | // https://thegraph.com/docs/en/developer/matchstick/#write-a-unit-test 39 | 40 | test("OwnershipTransferred created and stored", () => { 41 | assert.entityCount("OwnershipTransferred", 1) 42 | 43 | // 0xa16081f360e3847006db660bae1c6d1b2e17ec2a is the default address used in newMockEvent() function 44 | assert.fieldEquals( 45 | "OwnershipTransferred", 46 | "0xa16081f360e3847006db660bae1c6d1b2e17ec2a-1", 47 | "previousOwner", 48 | "0x0000000000000000000000000000000000000001" 49 | ) 50 | assert.fieldEquals( 51 | "OwnershipTransferred", 52 | "0xa16081f360e3847006db660bae1c6d1b2e17ec2a-1", 53 | "newOwner", 54 | "0x0000000000000000000000000000000000000001" 55 | ) 56 | 57 | // More assert options: 58 | // https://thegraph.com/docs/en/developer/matchstick/#asserts 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /.taskmaster/tasks/task_023.txt: -------------------------------------------------------------------------------- 1 | # Task ID: 23 2 | # Title: Implement ensemble agents update command 3 | # Status: pending 4 | # Dependencies: 21, 20 5 | # Priority: high 6 | # Description: Implement the 'ensemble agents update' command for updating existing agent records with multiple properties at once 7 | # Details: 8 | Create a comprehensive update command that allows users to modify agent records on the blockchain. The command should: 9 | 10 | 1. Support updating multiple properties via CLI options: 11 | - --name: Update agent name 12 | - --description: Update agent description 13 | - --category: Update agent category 14 | - --attributes: Update attributes (comma-separated) 15 | - --status: Update agent status (active/inactive/maintenance) 16 | - --image-uri: Update agent avatar 17 | - --communication-type: Update communication type 18 | - --communication-url: Update communication endpoint 19 | - Social links: --twitter, --telegram, --github, --website 20 | 21 | 2. Support bulk updates from configuration file: 22 | - --config : Load updates from YAML/JSON file 23 | - Validate configuration file format 24 | - Show diff between current and new values 25 | 26 | 3. Implementation requirements: 27 | - Use SDK's updateAgentRecord method (needs to be implemented in SDK) 28 | - Validate agent ownership before allowing updates 29 | - Show preview of changes before submitting 30 | - Require confirmation (unless --confirm flag) 31 | - Support --dry-run for testing 32 | - Handle gas estimation and custom gas limits 33 | 34 | 4. User experience: 35 | - Clear progress indicators during blockchain operations 36 | - Detailed error messages for common failures 37 | - Success confirmation with transaction details 38 | - Suggest next steps after update 39 | 40 | # Test Strategy: 41 | 1. Unit tests for update command parsing and validation 42 | 2. Integration tests with mock SDK responses 43 | 3. End-to-end tests on testnet with real agent updates 44 | 4. Test error scenarios: non-existent agents, permission denied, invalid data 45 | 5. Test configuration file parsing and validation 46 | 6. Test dry-run and confirmation flows 47 | -------------------------------------------------------------------------------- /packages/cli/src/utils/sdk.ts: -------------------------------------------------------------------------------- 1 | import { Ensemble } from '@ensemble-ai/sdk'; 2 | import { ethers } from 'ethers'; 3 | import { PinataSDK } from 'pinata-web3'; 4 | import { getConfig } from '../config/manager'; 5 | 6 | export async function createSDKInstance(providedSigner?: ethers.Signer): Promise { 7 | const config = await getConfig(); 8 | 9 | // Create provider 10 | const provider = new ethers.JsonRpcProvider(config.rpcUrl); 11 | 12 | // Use provided signer or create one 13 | let signer: ethers.Signer; 14 | if (providedSigner) { 15 | signer = providedSigner; 16 | } else if (config.privateKey) { 17 | signer = new ethers.Wallet(config.privateKey, provider); 18 | } else { 19 | // Use a dummy signer for read-only operations 20 | signer = ethers.Wallet.createRandom().connect(provider); 21 | } 22 | 23 | // Validate required subgraphUrl 24 | if (!config.subgraphUrl) { 25 | throw new Error('subgraphUrl is required in CLI config. Please ensure your config includes a subgraphUrl.'); 26 | } 27 | 28 | // Create ensemble config 29 | const ensembleConfig = { 30 | taskRegistryAddress: config.contracts.taskRegistry, 31 | agentRegistryAddress: config.contracts.agentRegistry, 32 | serviceRegistryAddress: config.contracts.serviceRegistry, 33 | network: { 34 | chainId: config.network === 'mainnet' ? 8453 : 84532, // Base mainnet : Base Sepolia 35 | name: config.network, 36 | rpcUrl: config.rpcUrl 37 | }, 38 | subgraphUrl: config.subgraphUrl 39 | }; 40 | 41 | // Initialize Pinata SDK if credentials are available 42 | let pinataSDK: PinataSDK | undefined; 43 | 44 | const pinataJwt = config.pinata?.jwt; 45 | const pinataGateway = config.pinata?.gateway; 46 | 47 | if (pinataJwt && pinataGateway) { 48 | pinataSDK = new PinataSDK({ 49 | pinataJwt, 50 | pinataGateway 51 | }); 52 | } 53 | 54 | return Ensemble.create(ensembleConfig, signer, pinataSDK); 55 | } 56 | 57 | export function createSignerFromPrivateKey(privateKey: string, rpcUrl: string): ethers.Signer { 58 | const provider = new ethers.JsonRpcProvider(rpcUrl); 59 | return new ethers.Wallet(privateKey, provider); 60 | } -------------------------------------------------------------------------------- /packages/subgraph/tests/agents-registry-utils.ts: -------------------------------------------------------------------------------- 1 | import { newMockEvent } from "matchstick-as" 2 | import { ethereum, Address, BigInt } from "@graphprotocol/graph-ts" 3 | import { 4 | AgentRegistered, 5 | OwnershipTransferred, 6 | ReputationUpdated 7 | } from "../generated/AgentsRegistry/AgentsRegistry" 8 | 9 | export function createAgentRegisteredEvent( 10 | agent: Address, 11 | model: string 12 | ): AgentRegistered { 13 | let agentRegisteredEvent = changetype(newMockEvent()) 14 | 15 | agentRegisteredEvent.parameters = new Array() 16 | 17 | agentRegisteredEvent.parameters.push( 18 | new ethereum.EventParam("agent", ethereum.Value.fromAddress(agent)) 19 | ) 20 | agentRegisteredEvent.parameters.push( 21 | new ethereum.EventParam("model", ethereum.Value.fromString(model)) 22 | ) 23 | 24 | return agentRegisteredEvent 25 | } 26 | 27 | export function createOwnershipTransferredEvent( 28 | previousOwner: Address, 29 | newOwner: Address 30 | ): OwnershipTransferred { 31 | let ownershipTransferredEvent = changetype( 32 | newMockEvent() 33 | ) 34 | 35 | ownershipTransferredEvent.parameters = new Array() 36 | 37 | ownershipTransferredEvent.parameters.push( 38 | new ethereum.EventParam( 39 | "previousOwner", 40 | ethereum.Value.fromAddress(previousOwner) 41 | ) 42 | ) 43 | ownershipTransferredEvent.parameters.push( 44 | new ethereum.EventParam("newOwner", ethereum.Value.fromAddress(newOwner)) 45 | ) 46 | 47 | return ownershipTransferredEvent 48 | } 49 | 50 | export function createReputationUpdatedEvent( 51 | agent: Address, 52 | newReputation: BigInt 53 | ): ReputationUpdated { 54 | let reputationUpdatedEvent = changetype(newMockEvent()) 55 | 56 | reputationUpdatedEvent.parameters = new Array() 57 | 58 | reputationUpdatedEvent.parameters.push( 59 | new ethereum.EventParam("agent", ethereum.Value.fromAddress(agent)) 60 | ) 61 | reputationUpdatedEvent.parameters.push( 62 | new ethereum.EventParam( 63 | "newReputation", 64 | ethereum.Value.fromUnsignedBigInt(newReputation) 65 | ) 66 | ) 67 | 68 | return reputationUpdatedEvent 69 | } 70 | -------------------------------------------------------------------------------- /packages/python-sdk/src/services/task_service.py: -------------------------------------------------------------------------------- 1 | 2 | from typing import Dict, Optional 3 | from web3 import Web3 4 | from web3.contract import Contract 5 | from eth_typing import Address 6 | 7 | class TaskService: 8 | def __init__(self, task_registry: Contract, account: Address): 9 | self.task_registry = task_registry 10 | self.account = account 11 | 12 | async def create_task(self, params: Dict) -> int: 13 | tx = await self.task_registry.functions.createTask( 14 | params["prompt"], 15 | params["task_type"] 16 | ).transact({'from': self.account.address}) 17 | receipt = await self.task_registry.web3.eth.wait_for_transaction_receipt(tx) 18 | 19 | task_id = self._find_event_in_receipt(receipt, "TaskCreated") 20 | if not task_id: 21 | raise Exception("Task creation failed: No task ID in event") 22 | return task_id 23 | 24 | async def get_task_data(self, task_id: str) -> Dict: 25 | task = await self.task_registry.functions.tasks(task_id).call() 26 | return { 27 | "id": task[0], 28 | "prompt": task[1], 29 | "task_type": task[2], 30 | "owner": task[3], 31 | "status": task[4], 32 | "assignee": task[5] 33 | } 34 | 35 | async def get_tasks_by_owner(self, owner: str) -> list: 36 | return await self.task_registry.functions.getTasksByOwner(owner).call() 37 | 38 | async def complete_task(self, task_id: int, result: str) -> None: 39 | try: 40 | tx = await self.task_registry.functions.completeTask( 41 | task_id, 42 | result 43 | ).transact({'from': self.account.address}) 44 | await self.task_registry.web3.eth.wait_for_transaction_receipt(tx) 45 | except Exception as e: 46 | print(f"Completing task failed: {str(e)}") 47 | raise 48 | 49 | def _find_event_in_receipt(self, receipt: dict, event_name: str) -> Optional[dict]: 50 | for log in receipt['logs']: 51 | try: 52 | event = self.task_registry.events[event_name]().process_log(log) 53 | if event: 54 | return event['args']['taskId'] 55 | except: 56 | continue 57 | return None 58 | -------------------------------------------------------------------------------- /packages/sdk/src/services/ServiceRegistryService.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "ethers"; 2 | import { Service } from "../types"; 3 | import { ServiceAlreadyRegisteredError } from "../errors"; 4 | import { ServiceRegistry } from "../../typechain"; 5 | 6 | export class ServiceRegistryService { 7 | constructor( 8 | private readonly serviceRegistry: ServiceRegistry, 9 | private signer?: ethers.Signer 10 | ) {} 11 | 12 | /** 13 | * Set the signer for write operations 14 | * @param {ethers.Signer} signer - The signer to use for write operations 15 | */ 16 | setSigner(signer: ethers.Signer): void { 17 | this.signer = signer; 18 | } 19 | 20 | /** 21 | * Check if a signer is required for write operations 22 | * @private 23 | */ 24 | private requireSigner(): void { 25 | if (!this.signer) { 26 | throw new Error("Signer required for write operations. Call setSigner() first."); 27 | } 28 | } 29 | 30 | /** 31 | * @param service The service to register 32 | * @returns A promise that resolves when the service is registered 33 | */ 34 | async registerService(service: Service): Promise { 35 | this.requireSigner(); 36 | try { 37 | console.log(`Registering service: ${service.name}`); 38 | 39 | const tx = await this.serviceRegistry.registerService( 40 | service.name, 41 | service.category, 42 | service.description 43 | ); 44 | console.log(`Transaction sent for service ${service.name}: ${tx.hash}`); 45 | 46 | const receipt = await tx.wait(); 47 | console.log( 48 | `Transaction confirmed for service ${service.name}: ${receipt?.hash}` 49 | ); 50 | 51 | return true; 52 | } catch (error: any) { 53 | console.error(`Error registering service ${service.name}:`, error); 54 | if (error.reason === "Service already registered") { 55 | throw new ServiceAlreadyRegisteredError(service.name); 56 | } 57 | throw error; 58 | } 59 | } 60 | /** 61 | * Gets a service by name. 62 | * @param {string} name - The name of the service. 63 | * @returns {Promise} A promise that resolves to the service. 64 | */ 65 | async getService(name: string): Promise { 66 | const service = await this.serviceRegistry.getService(name); 67 | return service; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /packages/sdk/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ensemble-ai/sdk", 3 | "version": "0.6.2", 4 | "description": "TypeScript SDK for the Ensemble Framework", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "files": [ 8 | "dist" 9 | ], 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/ensemble-codes/ensemble-framework.git", 13 | "directory": "packages/sdk" 14 | }, 15 | "publishConfig": { 16 | "access": "public", 17 | "registry": "https://registry.npmjs.org/" 18 | }, 19 | "license": "MIT", 20 | "homepage": "https://github.com/ensemble-codes/ensemble-framework/tree/main/packages/sdk", 21 | "bugs": { 22 | "url": "https://github.com/ensemble-codes/ensemble-framework/issues" 23 | }, 24 | "scripts": { 25 | "clean": "rm -rf dist typechain", 26 | "build": "npm run typechain && tsc", 27 | "watch": "tsc --watch", 28 | "test": "npm run typechain && jest --verbose", 29 | "test:agent": "npm run typechain && jest test/agentService.test.ts", 30 | "test:integration": "npm run typechain && npx tsx scripts/test-agent-methods.ts", 31 | "typecheck": "npm run typechain && tsc --noEmit", 32 | "docs": "typedoc", 33 | "upload-docs": "aws s3 cp ./docs s3://$ENSEMBLE_S3_BUCKET_NAME --recursive", 34 | "register-services": "ts-node ./scripts/register-services.ts", 35 | "create-proposals": "ts-node ./scripts/create-proposals.ts", 36 | "register-agents": "ts-node ./scripts/register-agents.ts", 37 | "typechain": "typechain --target ethers-v6 ./src/abi/*.json --out-dir ./typechain" 38 | }, 39 | "dependencies": { 40 | "@jest/globals": "^29.7.0", 41 | "chai": "^4.3.6", 42 | "dotenv": "^16.4.7", 43 | "ethers": "^6.9.0", 44 | "graphql": "^16.9.0", 45 | "graphql-request": "^7.1.2", 46 | "jest": "^29.7.0", 47 | "pinata-web3": "^0.5.4", 48 | "zod": "^4.0.17" 49 | }, 50 | "devDependencies": { 51 | "@babel/preset-typescript": "^7.26.0", 52 | "@graphprotocol/client-cli": "^3.0.7", 53 | "@swc/jest": "^0.2.37", 54 | "@typechain/ethers-v6": "^0.5.1", 55 | "@types/chai": "^5.0.1", 56 | "@types/jest": "^29.5.14", 57 | "@types/node": "^20.10.0", 58 | "jest": "^29.7.0", 59 | "ts-jest": "^29.2.5", 60 | "typechain": "^8.3.2", 61 | "typedoc": "^0.27.4", 62 | "typescript": "^5.3.2" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /packages/subgraph/src/agents-registry.ts: -------------------------------------------------------------------------------- 1 | import { BigInt } from "@graphprotocol/graph-ts"; 2 | import { 3 | AgentRegistered, 4 | ProposalAdded, 5 | ReputationUpdated, 6 | AgentDataUpdated 7 | } from "../generated/AgentsRegistry/AgentsRegistry" 8 | import { 9 | Agent, 10 | Proposal 11 | } from "../generated/schema" 12 | import { getContentPath } from "./utils"; 13 | import { IpfsContent } from "../generated/templates" 14 | 15 | export function handleAgentRegistered(event: AgentRegistered): void { 16 | let entity = new Agent( 17 | event.params.agent.toHex() 18 | ) 19 | 20 | entity.name = event.params.name; 21 | entity.owner = event.params.owner; 22 | entity.agentUri = event.params.agentUri; 23 | entity.reputation = BigInt.fromString('0'); 24 | 25 | let contentPath = getContentPath(event.params.agentUri); 26 | 27 | if (contentPath != "") { 28 | entity.metadata = contentPath; 29 | IpfsContent.create(contentPath); 30 | } 31 | 32 | entity.save(); 33 | } 34 | 35 | export function handleUpdateReputation(event: ReputationUpdated): void { 36 | let entity = Agent.load(event.params.agent.toHex()); 37 | if (entity == null) { 38 | return 39 | } 40 | 41 | entity.reputation = event.params.newReputation; 42 | 43 | entity.save(); 44 | } 45 | 46 | export function handleProposalAdded(event: ProposalAdded): void { 47 | let entity = new Proposal(event.params.proposalId.toString()); 48 | 49 | entity.issuer = event.params.agent.toHex(); 50 | entity.price = event.params.price; 51 | entity.service = event.params.name; 52 | entity.isRemoved = false; 53 | entity.tokenAddress = event.params.tokenAddress; 54 | 55 | entity.save() 56 | } 57 | 58 | export function handleProposalRemoved(event: ProposalAdded): void { 59 | let entity = Proposal.load(event.params.proposalId.toString()); 60 | if (entity == null) { 61 | return 62 | } 63 | 64 | entity.isRemoved = true; 65 | 66 | entity.save() 67 | } 68 | 69 | export function handleAgentDataUpdated(event: AgentDataUpdated): void { 70 | let entity = Agent.load(event.params.agent.toHex()); 71 | if (entity == null) { 72 | return 73 | } 74 | 75 | entity.name = event.params.name; 76 | entity.agentUri = event.params.agentUri; 77 | 78 | // Update metadata if agentUri has changed 79 | let contentPath = getContentPath(event.params.agentUri); 80 | if (contentPath != "") { 81 | entity.metadata = contentPath; 82 | IpfsContent.create(contentPath); 83 | } 84 | 85 | entity.save(); 86 | } 87 | -------------------------------------------------------------------------------- /packages/cli/src/bin/ensemble.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { Command } from 'commander'; 4 | import chalk from 'chalk'; 5 | import { config } from 'dotenv'; 6 | import { getConfig } from '../config/manager'; 7 | import { walletCommand } from '../commands/wallets'; 8 | import { agentsCommand } from '../commands/agents'; 9 | import { initCommand } from '../commands/init'; 10 | import { validateCommand } from '../commands/validate'; 11 | 12 | // Load environment variables 13 | config(); 14 | 15 | const program = new Command(); 16 | 17 | program 18 | .name('ensemble') 19 | .description('Ensemble CLI - Command-line interface for agent management') 20 | .version('0.1.0'); 21 | 22 | // Global options 23 | program 24 | .option('--verbose', 'Enable verbose output') 25 | .option('--format ', 'Output format (table, json, csv, yaml)', 'yaml') 26 | .option('--wallet ', 'Override active wallet for this command'); 27 | 28 | // Add agents command 29 | program.addCommand(agentsCommand); 30 | 31 | // Config command 32 | program 33 | .command('config') 34 | .description('Show CLI configuration') 35 | .action(async (_options, command) => { 36 | try { 37 | const globalOptions = command.parent.opts(); 38 | const config = await getConfig(); 39 | const { formatOutput } = await import('../utils/formatters'); 40 | 41 | // Remove sensitive information for display 42 | const displayConfig = { ...config }; 43 | if (displayConfig.privateKey) { 44 | displayConfig.privateKey = '***HIDDEN***'; 45 | } 46 | 47 | const output = formatOutput([displayConfig], globalOptions.format); 48 | console.log(output); 49 | } catch (error: any) { 50 | console.error(chalk.red('❌ Error reading configuration:')); 51 | console.error(chalk.red(error.message)); 52 | if (command.parent.opts().verbose) { 53 | console.error(error.stack); 54 | } 55 | process.exit(1); 56 | } 57 | }); 58 | 59 | // Add init command 60 | program.addCommand(initCommand) 61 | 62 | // Add validate command 63 | program.addCommand(validateCommand) 64 | 65 | // Add wallet command 66 | program.addCommand(walletCommand); 67 | 68 | // Error handling 69 | program.on('command:*', () => { 70 | console.error(chalk.red(`Unknown command: ${program.args.join(' ')}`)); 71 | console.log('See --help for available commands.'); 72 | process.exit(1); 73 | }); 74 | 75 | // Parse arguments 76 | program.parse(); 77 | 78 | // Show help if no command provided 79 | if (!process.argv.slice(2).length) { 80 | program.outputHelp(); 81 | } -------------------------------------------------------------------------------- /packages/python-sdk/src/services/agent_service.py: -------------------------------------------------------------------------------- 1 | 2 | from decimal import Decimal 3 | from typing import List, Optional 4 | from web3 import Web3 5 | from web3.contract import Contract 6 | from eth_typing import Address 7 | from ..types import AgentData 8 | 9 | class AgentService: 10 | def __init__(self, agent_registry: Contract, account: Address): 11 | self.agent_registry = agent_registry 12 | self.account = account 13 | 14 | async def get_address(self) -> str: 15 | return self.account.address 16 | 17 | async def register_agent(self, model: str, prompt: str, skills: List[str]) -> str: 18 | tx = await self.agent_registry.functions.registerAgent( 19 | model, prompt, skills 20 | ).transact({'from': self.account}) 21 | receipt = await self.agent_registry.web3.eth.wait_for_transaction_receipt(tx) 22 | 23 | event = self._find_event_in_receipt(receipt, "AgentRegistered") 24 | if not event or not event['args']: 25 | raise Exception("Agent registration failed") 26 | return event['args'][0] 27 | 28 | async def get_agent_data(self, agent_address: str) -> AgentData: 29 | data = await self.agent_registry.functions.getAgentData(agent_address).call() 30 | is_registered = await self.agent_registry.functions.isRegistered(agent_address).call() 31 | 32 | return AgentData( 33 | address=agent_address, 34 | model=data[0], 35 | prompt=data[1], 36 | skills=data[2], 37 | reputation=Decimal(data[3]), 38 | is_registered=is_registered 39 | ) 40 | 41 | async def is_agent_registered(self, agent_address: str) -> bool: 42 | return await self.agent_registry.functions.isRegistered(agent_address).call() 43 | 44 | async def add_agent_skill(self, name: str, level: int) -> None: 45 | tx = await self.agent_registry.functions.addSkill(name, level).transact({'from': self.account}) 46 | await self.agent_registry.web3.eth.wait_for_transaction_receipt(tx) 47 | 48 | async def update_agent_reputation(self, reputation: Decimal) -> None: 49 | tx = await self.agent_registry.functions.updateReputation(int(reputation)).transact({'from': self.account}) 50 | await self.agent_registry.web3.eth.wait_for_transaction_receipt(tx) 51 | 52 | def _find_event_in_receipt(self, receipt: dict, event_name: str) -> Optional[dict]: 53 | for log in receipt['logs']: 54 | try: 55 | event = self.agent_registry.events[event_name]().process_log(log) 56 | return event 57 | except: 58 | continue 59 | return None 60 | -------------------------------------------------------------------------------- /packages/python-sdk/src/abi/AgentsRegistry.abi.json: -------------------------------------------------------------------------------- 1 | 2 | [ 3 | { 4 | "inputs": [], 5 | "stateMutability": "nonpayable", 6 | "type": "constructor" 7 | }, 8 | { 9 | "anonymous": false, 10 | "inputs": [ 11 | { 12 | "indexed": true, 13 | "internalType": "address", 14 | "name": "agent", 15 | "type": "address" 16 | }, 17 | { 18 | "indexed": false, 19 | "internalType": "string", 20 | "name": "model", 21 | "type": "string" 22 | } 23 | ], 24 | "name": "AgentRegistered", 25 | "type": "event" 26 | }, 27 | { 28 | "inputs": [ 29 | { 30 | "internalType": "string", 31 | "name": "name", 32 | "type": "string" 33 | }, 34 | { 35 | "internalType": "uint256", 36 | "name": "level", 37 | "type": "uint256" 38 | } 39 | ], 40 | "name": "addSkill", 41 | "outputs": [], 42 | "stateMutability": "nonpayable", 43 | "type": "function" 44 | }, 45 | { 46 | "inputs": [ 47 | { 48 | "internalType": "address", 49 | "name": "agent", 50 | "type": "address" 51 | } 52 | ], 53 | "name": "getAgentData", 54 | "outputs": [ 55 | { 56 | "internalType": "string", 57 | "name": "model", 58 | "type": "string" 59 | }, 60 | { 61 | "internalType": "string", 62 | "name": "prompt", 63 | "type": "string" 64 | }, 65 | { 66 | "components": [ 67 | { 68 | "internalType": "string", 69 | "name": "name", 70 | "type": "string" 71 | }, 72 | { 73 | "internalType": "uint256", 74 | "name": "level", 75 | "type": "uint256" 76 | } 77 | ], 78 | "internalType": "struct AgentsRegistry.Skill[]", 79 | "name": "skills", 80 | "type": "tuple[]" 81 | }, 82 | { 83 | "internalType": "uint256", 84 | "name": "reputation", 85 | "type": "uint256" 86 | } 87 | ], 88 | "stateMutability": "view", 89 | "type": "function" 90 | }, 91 | { 92 | "inputs": [ 93 | { 94 | "internalType": "address", 95 | "name": "agent", 96 | "type": "address" 97 | } 98 | ], 99 | "name": "isRegistered", 100 | "outputs": [ 101 | { 102 | "internalType": "bool", 103 | "name": "", 104 | "type": "bool" 105 | } 106 | ], 107 | "stateMutability": "view", 108 | "type": "function" 109 | } 110 | ] 111 | -------------------------------------------------------------------------------- /packages/contracts/memory-bank/productContext.md: -------------------------------------------------------------------------------- 1 | # Product Context: Ensemble Framework 2 | 3 | ## Why This Project Exists 4 | The Ensemble Framework addresses the critical need for decentralized infrastructure supporting AI agent economies. As AI agents become more autonomous and capable, they require: 5 | 6 | 1. **Identity and Reputation Systems** - Verifiable on-chain identities for trust and accountability 7 | 2. **Task Coordination** - Decentralized mechanisms for task creation, assignment, and completion 8 | 3. **Service Discovery** - Marketplace for agents to offer and discover capabilities 9 | 4. **Economic Incentives** - Token-based rewards and payments for agent services 10 | 11 | ## Problems We Solve 12 | 13 | ### Current Pain Points 14 | - **Centralized AI Platforms**: Single points of failure and control 15 | - **Limited Agent Interoperability**: Siloed AI systems that can't collaborate 16 | - **Trust and Verification**: No reliable way to verify agent capabilities or performance 17 | - **Economic Barriers**: Lack of efficient payment rails for AI services 18 | 19 | ### Our Solution 20 | A decentralized, on-chain infrastructure that enables: 21 | - Trustless agent registration and verification 22 | - Transparent task execution and completion tracking 23 | - Open service marketplace with built-in reputation systems 24 | - Efficient micro-payments and economic incentives 25 | 26 | ## How It Should Work 27 | 28 | ### Agent Lifecycle 29 | 1. **Registration** - Agents register their identities and capabilities on-chain 30 | 2. **Service Offering** - Agents list their available services in the registry 31 | 3. **Task Discovery** - Agents find relevant tasks through the task registry 32 | 4. **Execution** - Tasks are completed with transparent on-chain tracking 33 | 5. **Payment** - Automatic settlement through smart contracts 34 | 35 | ### User Experience Goals 36 | - **Simple Integration** - Easy SDK for developers to integrate agents 37 | - **Low Friction** - Minimal gas costs for routine operations 38 | - **Transparency** - Full visibility into agent activities and performance 39 | - **Reliability** - Robust error handling and recovery mechanisms 40 | - **Scalability** - Support for thousands of concurrent agents 41 | 42 | ## Target Users 43 | 1. **AI Agent Developers** - Building autonomous agents for various use cases 44 | 2. **Service Consumers** - Organizations needing AI services 45 | 3. **Token Holders** - Participants in the economic ecosystem 46 | 4. **Infrastructure Providers** - Supporting the network with resources 47 | 48 | ## Success Metrics 49 | - Number of registered agents 50 | - Transaction volume through the system 51 | - Task completion rates 52 | - Network uptime and reliability 53 | - Developer adoption and integration -------------------------------------------------------------------------------- /packages/mcp-server/README.md: -------------------------------------------------------------------------------- 1 | # Ensemble Agent Matcher MCP Server 2 | 3 | This is a Model Context Protocol (MCP) server that provides tools to find and match AI agents in the Ensemble network based on user requirements. 4 | 5 | ## Features 6 | 7 | - Searches for agents based on service descriptions as a priority 8 | - Falls back to agent descriptions when service matches are insufficient 9 | - Ranks agents by a scoring system that combines match relevance and agent reputation 10 | - Provides detailed agent information including proposals, services, and task history 11 | 12 | ## Getting Started 13 | 14 | ### Prerequisites 15 | 16 | - Node.js 18 or higher 17 | - npm or pnpm 18 | 19 | ### Installation 20 | 21 | 1. Install dependencies: 22 | ```bash 23 | pnpm install 24 | ``` 25 | 26 | 2. Build the project: 27 | ```bash 28 | pnpm build 29 | ``` 30 | 31 | 3. Start the server: 32 | ```bash 33 | pnpm start 34 | ``` 35 | 36 | The server will start on port 3000 by default. You can change the port by setting the `PORT` environment variable. 37 | 38 | ## API Tools 39 | 40 | ### findAgents 41 | 42 | Find agents that match a user's request, prioritizing: 43 | 1. Service descriptions first 44 | 2. Agent descriptions second 45 | 46 | ```typescript 47 | findAgents({ 48 | query: string; // The user request or search query 49 | limit?: number; // Maximum number of agents to return (default: 5) 50 | minReputationScore?: number; // Minimum reputation score (default: 0) 51 | }) 52 | ``` 53 | 54 | Returns a list of matched agents sorted by relevance score. 55 | 56 | ### getAgentDetails 57 | 58 | Get detailed information about a specific agent. 59 | 60 | ```typescript 61 | getAgentDetails({ 62 | agentId: string; // The ID of the agent to get details for 63 | }) 64 | ``` 65 | 66 | Returns comprehensive agent details including profile, proposals, tasks, and more. 67 | 68 | ## How Matching Works 69 | 70 | 1. The server first searches for services that match the user's query by examining service descriptions 71 | 2. For each matching service, it finds agents that offer this service through their proposals 72 | 3. Agents are scored based on their reputation plus a bonus for matching services 73 | 4. If more results are needed, the server searches for agents whose names or descriptions match the query 74 | 5. Results are sorted by score, with the highest scoring agents first 75 | 76 | ## Integration with MCP Clients 77 | 78 | This server can be used with any MCP-compatible client. Example using TypeScript: 79 | 80 | ```typescript 81 | import { createClient } from '@mcp/client'; 82 | 83 | const client = createClient({ 84 | url: 'http://localhost:3000' 85 | }); 86 | 87 | // Find agents matching a request 88 | const result = await client.tools.findAgents({ 89 | query: "Create a Twitter thread about AI safety", 90 | limit: 3 91 | }); 92 | 93 | console.log(result); 94 | ``` -------------------------------------------------------------------------------- /packages/cli/src/utils/file-operations.ts: -------------------------------------------------------------------------------- 1 | import { writeFile, mkdir } from 'fs/promises'; 2 | import { existsSync } from 'fs'; 3 | import { join, dirname } from 'path'; 4 | import { stringify as yamlStringify } from 'yaml'; 5 | import { AgentRecordYAML } from '../types/config'; 6 | 7 | export async function saveAgentRecords( 8 | agents: any[], 9 | directory: string, 10 | prefix: string = 'agent-record' 11 | ): Promise { 12 | // Ensure directory exists 13 | if (!existsSync(directory)) { 14 | await mkdir(directory, { recursive: true }); 15 | } 16 | 17 | for (let i = 0; i < agents.length; i++) { 18 | const agent = agents[i]; 19 | const filename = agents.length === 1 20 | ? `${prefix}.yaml` 21 | : `${prefix}-${i + 1}.yaml`; 22 | 23 | const filepath = join(directory, filename); 24 | const yamlContent = convertAgentToYAML(agent); 25 | 26 | await writeFile(filepath, yamlContent, 'utf-8'); 27 | } 28 | } 29 | 30 | export async function saveAgentRecord( 31 | agent: any, 32 | filepath: string 33 | ): Promise { 34 | // Ensure directory exists 35 | const dir = dirname(filepath); 36 | if (!existsSync(dir)) { 37 | await mkdir(dir, { recursive: true }); 38 | } 39 | 40 | const yamlContent = convertAgentToYAML(agent); 41 | await writeFile(filepath, yamlContent, 'utf-8'); 42 | } 43 | 44 | function convertAgentToYAML(agent: any): string { 45 | const agentRecord: AgentRecordYAML = { 46 | name: agent.name || 'Unknown Agent', 47 | description: agent.description || '', 48 | category: agent.category || 'general', 49 | attributes: agent.attributes || [], 50 | instructions: agent.instructions || [], 51 | prompts: agent.prompts || [], 52 | imageURI: agent.imageURI || '', 53 | communication: { 54 | type: agent.communicationType || 'eliza', 55 | params: agent.communicationParams || {} 56 | }, 57 | socials: { 58 | twitter: agent.socials?.twitter || '', 59 | telegram: agent.socials?.telegram || '', 60 | github: agent.socials?.github || '', 61 | website: agent.socials?.website || '', 62 | dexscreener: agent.socials?.dexscreener || '' 63 | }, 64 | status: 'active' // Default status 65 | }; 66 | 67 | // Add comment header 68 | const header = `# Agent Record Configuration 69 | # This file defines the configuration for an Ensemble agent 70 | # Edit the values below and use 'ensemble register agent --config ' to register 71 | # 72 | # Generated on: ${new Date().toISOString()} 73 | # Agent Address: ${agent.address || 'Not yet registered'} 74 | 75 | `; 76 | 77 | return header + yamlStringify(agentRecord, { 78 | indent: 2, 79 | lineWidth: 80, 80 | minContentWidth: 20 81 | }); 82 | } 83 | 84 | export async function ensureDirectoryExists(directory: string): Promise { 85 | if (!existsSync(directory)) { 86 | await mkdir(directory, { recursive: true }); 87 | } 88 | } -------------------------------------------------------------------------------- /packages/contracts/test/ServiceRegistry.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require("chai"); 2 | const { ethers, upgrades } = require("hardhat"); 3 | 4 | describe("ServiceRegistryUpgradeable", function () { 5 | let ServiceRegistry; 6 | let registry; 7 | let owner, addr1; 8 | 9 | let service1 = "Service1"; 10 | let category1 = "Category1"; 11 | let description1 = "Description1"; 12 | 13 | let category2 = "Category2"; 14 | let description2 = "Description2"; 15 | 16 | beforeEach(async function () { 17 | [owner, addr1] = await ethers.getSigners(); 18 | ServiceRegistry = await ethers.getContractFactory("ServiceRegistryUpgradeable"); 19 | registry = await upgrades.deployProxy(ServiceRegistry, [], { 20 | initializer: "initialize", 21 | kind: "uups" 22 | }); 23 | }); 24 | 25 | it("Should register a new service", async function () { 26 | const request = registry.registerService(service1, category1, description1) 27 | 28 | await expect(request) 29 | .to.emit(registry, "ServiceRegistered") 30 | .withArgs(service1, category1, description1); 31 | 32 | const isRegistered = await registry.isServiceRegistered(service1); 33 | expect(isRegistered).to.be.true; 34 | 35 | const service = await registry.getService(service1); 36 | expect(service.name).to.equal(service1); 37 | expect(service.category).to.equal(category1); 38 | expect(service.description).to.equal(description1); 39 | }); 40 | 41 | it("Should not register a service twice", async function () { 42 | await registry.registerService(service1, category1, description1); 43 | await expect(registry.registerService(service1, category1, description1)).to.be.revertedWith("Service already registered"); 44 | }); 45 | 46 | it("Should not register a service with an empty name", async function () { 47 | await expect(registry.registerService("", category1, description1)).to.be.revertedWith("Invalid service name"); 48 | }); 49 | 50 | it("Should not update a service if it is not registered", async function () { 51 | await expect(registry.updateService(service1, category2, description2)).to.be.revertedWith("Service not registered"); 52 | }); 53 | 54 | 55 | it("Should update a service", async function () { 56 | await registry.registerService(service1, category1, description1); 57 | const request = registry.updateService(service1, category2, description2); 58 | 59 | await expect(request) 60 | .to.emit(registry, "ServiceUpdated") 61 | .withArgs(service1, category2, description2); 62 | 63 | const service = await registry.getService(service1); 64 | expect(service.name).to.equal(service1); 65 | expect(service.category).to.equal(category2); 66 | expect(service.description).to.equal(description2); 67 | 68 | }); 69 | 70 | }) -------------------------------------------------------------------------------- /packages/contracts/ignition/modules/DeployAll.ts: -------------------------------------------------------------------------------- 1 | import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; 2 | import EnsembleCreditsModule from "./EnsembleCredits"; 3 | import ServiceRegistryUpgradeableModule from "./ServiceRegistryUpgradeable"; 4 | import AgentsRegistryUpgradeableModule from "./AgentsRegistryUpgradeable"; 5 | import TaskRegistryUpgradeableModule from "./TaskRegistryUpgradeable"; 6 | 7 | /** 8 | * Master deployment module for the entire Ensemble Framework 9 | * 10 | * This module orchestrates the deployment of all contracts in the correct order: 11 | * 1. EnsembleCredits (independent ERC20 token) 12 | * 2. ServiceRegistry (base registry) 13 | * 3. AgentsRegistry (depends on ServiceRegistry) 14 | * 4. TaskRegistry (depends on ServiceRegistry and can integrate with EnsembleCredits) 15 | * 16 | * Parameters (all optional & overridable via CLI / ignition.json): 17 | * - tokenName (string) : ERC20 name for credits (default: "Ensemble Credits") 18 | * - tokenSymbol (string) : ERC20 symbol for credits (default: "EC") 19 | * - initialAdmin (address) : Admin for all contracts (default: deployer[0]) 20 | * - initialSupply (uint256) : Initial credits supply (default: 0) 21 | * - v1RegistryAddress (address): Legacy registry address (default: zero address) 22 | */ 23 | export default buildModule("DeployAllModule", (m) => { 24 | // Global parameters 25 | const initialAdmin = m.getParameter("initialAdmin", m.getAccount(0)); 26 | 27 | // Deploy EnsembleCredits token (independent) 28 | const { ensembleCredits } = m.useModule(EnsembleCreditsModule); 29 | 30 | // Deploy ServiceRegistry (base dependency) 31 | const { serviceRegistry } = m.useModule(ServiceRegistryUpgradeableModule); 32 | 33 | // Deploy AgentsRegistry (depends on ServiceRegistry) 34 | const { agentsRegistry, agentsRegistryProxy, agentsRegistryImpl } = m.useModule(AgentsRegistryUpgradeableModule); 35 | 36 | // Deploy TaskRegistry (depends on ServiceRegistry) 37 | const { taskRegistry, taskRegistryProxy, taskRegistryImpl } = m.useModule(TaskRegistryUpgradeableModule); 38 | 39 | // Optional: Set up initial integrations between contracts 40 | // Note: These are commented out as they depend on your specific business logic 41 | 42 | // Example: Grant minter role to TaskRegistry for automatic reward distribution 43 | // m.call(ensembleCredits, "grantRole", [ 44 | // m.staticCall(ensembleCredits, "MINTER_ROLE"), 45 | // taskRegistry 46 | // ], { id: "GrantMinterRoleToTaskRegistry" }); 47 | 48 | return { 49 | // ERC20 Token 50 | ensembleCredits, 51 | 52 | // Registry Contracts (Proxy Instances) 53 | serviceRegistry, 54 | agentsRegistry, 55 | taskRegistry, 56 | 57 | // Proxy Addresses (for upgrades) 58 | agentsRegistryProxy, 59 | taskRegistryProxy, 60 | 61 | // Implementation Addresses (for verification) 62 | agentsRegistryImpl, 63 | taskRegistryImpl 64 | }; 65 | }); -------------------------------------------------------------------------------- /packages/contracts/ignition/modules/UpgradeRegistries.ts: -------------------------------------------------------------------------------- 1 | import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; 2 | 3 | /** 4 | * Module for upgrading registry contracts 5 | * This demonstrates how to upgrade UUPS proxies using Hardhat Ignition 6 | * 7 | * Usage: 8 | * 1. Deploy new implementation contracts 9 | * 2. Call upgradeToAndCall on the existing proxies (OpenZeppelin v5 uses upgradeToAndCall instead of upgradeTo) 10 | * 11 | * Note: This assumes you have existing proxy addresses from previous deployments 12 | */ 13 | const UpgradeRegistriesModule = buildModule("UpgradeRegistriesModule", (m) => { 14 | // Get existing proxy addresses (should be provided as parameters) 15 | const serviceRegistryProxyAddress = m.getParameter("serviceRegistryProxy"); 16 | const agentsRegistryProxyAddress = m.getParameter("agentsRegistryProxy"); 17 | const taskRegistryProxyAddress = m.getParameter("taskRegistryProxy"); 18 | 19 | // Deploy new implementation contracts 20 | const newServiceRegistryImpl = m.contract("ServiceRegistryUpgradeable", [], { 21 | id: "NewServiceRegistryImpl" 22 | }); 23 | 24 | const newAgentsRegistryImpl = m.contract("AgentsRegistryUpgradeable", [], { 25 | id: "NewAgentsRegistryImpl" 26 | }); 27 | 28 | const newTaskRegistryImpl = m.contract("TaskRegistryUpgradeable", [], { 29 | id: "NewTaskRegistryImpl" 30 | }); 31 | 32 | // Get contract instances for existing proxies 33 | const serviceRegistryProxy = m.contractAt("ServiceRegistryUpgradeable", serviceRegistryProxyAddress, { 34 | id: "ExistingServiceRegistryProxy" 35 | }); 36 | 37 | const agentsRegistryProxy = m.contractAt("AgentsRegistryUpgradeable", agentsRegistryProxyAddress, { 38 | id: "ExistingAgentsRegistryProxy" 39 | }); 40 | 41 | const taskRegistryProxy = m.contractAt("TaskRegistryUpgradeable", taskRegistryProxyAddress, { 42 | id: "ExistingTaskRegistryProxy" 43 | }); 44 | 45 | // Perform upgrades by calling upgradeToAndCall on each proxy with empty data 46 | // Note: Only the proxy owner can perform these upgrades 47 | // In OpenZeppelin v5, upgradeTo was removed and upgradeToAndCall is used with empty data for simple upgrades 48 | m.call(serviceRegistryProxy, "upgradeToAndCall", [newServiceRegistryImpl, "0x"], { 49 | id: "UpgradeServiceRegistry" 50 | }); 51 | 52 | m.call(agentsRegistryProxy, "upgradeToAndCall", [newAgentsRegistryImpl, "0x"], { 53 | id: "UpgradeAgentsRegistry" 54 | }); 55 | 56 | m.call(taskRegistryProxy, "upgradeToAndCall", [newTaskRegistryImpl, "0x"], { 57 | id: "UpgradeTaskRegistry" 58 | }); 59 | 60 | return { 61 | newServiceRegistryImpl, 62 | newAgentsRegistryImpl, 63 | newTaskRegistryImpl, 64 | serviceRegistryProxy, 65 | agentsRegistryProxy, 66 | taskRegistryProxy 67 | }; 68 | }); 69 | 70 | export default UpgradeRegistriesModule; -------------------------------------------------------------------------------- /packages/subgraph/src/fds.ts: -------------------------------------------------------------------------------- 1 | import { Bytes, dataSource, json, JSONValue, JSONValueKind, TypedMap } from "@graphprotocol/graph-ts"; 2 | import { IpfsMetadata } from "../generated/schema"; 3 | 4 | export function handleAgentMetadata(content: Bytes): void { 5 | let metadataId = dataSource.stringParam() 6 | let ipfsData = IpfsMetadata.load(metadataId) 7 | 8 | if (ipfsData) { 9 | return 10 | } 11 | 12 | ipfsData = new IpfsMetadata(metadataId) 13 | 14 | if (content.length == 0) { 15 | saveDefaultMetadata(ipfsData) 16 | return 17 | } 18 | 19 | let jsonResult = json.try_fromBytes(content) 20 | if (jsonResult.isError) { 21 | saveDefaultMetadata(ipfsData) 22 | return 23 | } 24 | 25 | let value = jsonResult.value.toObject() 26 | if (!value) { 27 | saveDefaultMetadata(ipfsData) 28 | return 29 | } 30 | 31 | ipfsData.name = extractStringField(value, "name") 32 | ipfsData.description = extractStringField(value, "description") 33 | ipfsData.imageUri = extractStringField(value, "imageURI") 34 | ipfsData.agentCategory = extractStringField(value, "agentCategory") 35 | ipfsData.openingGreeting = extractStringField(value, "openingGreeting") 36 | ipfsData.communicationType = extractStringField(value, "communicationType") 37 | ipfsData.communicationURL = extractStringField(value, "communicationURL") 38 | ipfsData.communicationParams = extractStringField(value, "communicationParams") 39 | 40 | let socials = value.get("socials") 41 | if (!socials) { 42 | ipfsData.save() 43 | return 44 | } 45 | 46 | ipfsData.attributes = extractArrayField(value, "attributes") 47 | ipfsData.instructions = extractArrayField(value, "instructions") 48 | ipfsData.prompts = extractArrayField(value, "prompts") 49 | 50 | let socialsObj = socials.toObject() 51 | ipfsData.telegram = extractStringField(socialsObj, "telegram") 52 | ipfsData.twitter = extractStringField(socialsObj, "twitter") 53 | ipfsData.github = extractStringField(socialsObj, "github") 54 | ipfsData.dexscreener = extractStringField(socialsObj, "dexscreener") 55 | ipfsData.website = extractStringField(socialsObj, "website") 56 | 57 | ipfsData.save(); 58 | } 59 | 60 | function extractStringField(obj: TypedMap, field: string): string { 61 | let value = obj.get(field) 62 | return value && value.kind == JSONValueKind.STRING ? value.toString() : "" 63 | } 64 | 65 | function extractArrayField(obj: TypedMap, field: string): string[] { 66 | let value = obj.get(field) 67 | if (value && value.kind == JSONValueKind.ARRAY) { 68 | let array = value.toArray() 69 | let result: string[] = [] 70 | for (let i = 0; i < array.length; i++) { 71 | result.push(array[i].toString()) 72 | } 73 | return result 74 | } 75 | return [] 76 | } 77 | 78 | function saveDefaultMetadata(ipfsData: IpfsMetadata): void { 79 | ipfsData.name = "" 80 | ipfsData.description = "" 81 | ipfsData.imageUri = "" 82 | ipfsData.telegram = "" 83 | ipfsData.twitter = "" 84 | ipfsData.github = "" 85 | ipfsData.dexscreener = "" 86 | ipfsData.website = "" 87 | ipfsData.save() 88 | } 89 | -------------------------------------------------------------------------------- /packages/subgraph/subgraph.yaml: -------------------------------------------------------------------------------- 1 | specVersion: 1.2.0 2 | indexerHints: 3 | prune: auto 4 | schema: 5 | file: ./schema.graphql 6 | dataSources: 7 | - kind: ethereum 8 | name: ServiceRegistry 9 | network: base-sepolia 10 | source: 11 | address: "0x3Acbf1Ca047a18bE88E7160738A9B0bB64203244" 12 | abi: ServiceRegistry 13 | startBlock: 23026978 14 | mapping: 15 | kind: ethereum/events 16 | apiVersion: 0.0.7 17 | language: wasm/assemblyscript 18 | entities: 19 | - Service 20 | abis: 21 | - name: ServiceRegistry 22 | file: ./abis/ServiceRegistry.json 23 | eventHandlers: 24 | - event: ServiceRegistered(string,string,string) 25 | handler: handleServiceRegistered 26 | - event: ServiceUpdated(string,string,string) 27 | handler: handleServiceUpdated 28 | file: ./src/service-registry.ts 29 | - kind: ethereum 30 | name: AgentsRegistry 31 | network: base-sepolia 32 | source: 33 | address: "0xDbF645cC23066cc364C4Db915c78135eE52f11B2" 34 | abi: AgentsRegistry 35 | startBlock: 23026985 36 | mapping: 37 | kind: ethereum/events 38 | apiVersion: 0.0.7 39 | language: wasm/assemblyscript 40 | entities: 41 | - Agent 42 | - Proposal 43 | abis: 44 | - name: AgentsRegistry 45 | file: ./abis/AgentsRegistry.json 46 | eventHandlers: 47 | - event: AgentRegistered(indexed address,indexed address,string,string) 48 | handler: handleAgentRegistered 49 | - event: ReputationUpdated(indexed address,uint256) 50 | handler: handleUpdateReputation 51 | - event: ProposalAdded(indexed address,uint256,string,uint256,address) 52 | handler: handleProposalAdded 53 | - event: AgentDataUpdated(indexed address,string,string) 54 | handler: handleAgentDataUpdated 55 | file: ./src/agents-registry.ts 56 | - kind: ethereum 57 | name: TaskRegistry 58 | network: base-sepolia 59 | source: 60 | address: "0x847fA49b999489fD2780fe2843A7b1608106b49b" 61 | abi: TaskRegistry 62 | startBlock: 23026993 63 | mapping: 64 | kind: ethereum/events 65 | apiVersion: 0.0.7 66 | language: wasm/assemblyscript 67 | entities: 68 | - Task 69 | - Agent 70 | abis: 71 | - name: TaskRegistry 72 | file: ./abis/TaskRegistry.json 73 | eventHandlers: 74 | - event: TaskCreated(indexed address,indexed address,uint256,uint256,string) 75 | handler: handleTaskCreated 76 | - event: TaskStatusChanged(indexed uint256,uint8) 77 | handler: handleTaskStatusChanged 78 | - event: TaskCompleted(indexed uint256,string) 79 | handler: handleTaskStatusCompleted 80 | - event: TaskRated(indexed uint256,uint8) 81 | handler: handleTaskRated 82 | file: ./src/task-registry.ts 83 | templates: 84 | - kind: file/ipfs 85 | name: IpfsContent 86 | mapping: 87 | apiVersion: 0.0.7 88 | language: wasm/assemblyscript 89 | entities: 90 | - IpfsMetadata 91 | abis: 92 | - name: AgentsRegistry 93 | file: ./abis/AgentsRegistry.json 94 | handler: handleAgentMetadata 95 | file: ./src/fds.ts 96 | -------------------------------------------------------------------------------- /packages/sdk/docs/assets/highlight.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --light-hl-0: #795E26; 3 | --dark-hl-0: #DCDCAA; 4 | --light-hl-1: #000000; 5 | --dark-hl-1: #D4D4D4; 6 | --light-hl-2: #A31515; 7 | --dark-hl-2: #CE9178; 8 | --light-hl-3: #0000FF; 9 | --dark-hl-3: #569CD6; 10 | --light-hl-4: #0070C1; 11 | --dark-hl-4: #4FC1FF; 12 | --light-hl-5: #001080; 13 | --dark-hl-5: #9CDCFE; 14 | --light-hl-6: #008000; 15 | --dark-hl-6: #6A9955; 16 | --light-hl-7: #AF00DB; 17 | --dark-hl-7: #C586C0; 18 | --light-hl-8: #000000FF; 19 | --dark-hl-8: #D4D4D4; 20 | --light-hl-9: #267F99; 21 | --dark-hl-9: #4EC9B0; 22 | --light-hl-10: #098658; 23 | --dark-hl-10: #B5CEA8; 24 | --light-code-background: #FFFFFF; 25 | --dark-code-background: #1E1E1E; 26 | } 27 | 28 | @media (prefers-color-scheme: light) { :root { 29 | --hl-0: var(--light-hl-0); 30 | --hl-1: var(--light-hl-1); 31 | --hl-2: var(--light-hl-2); 32 | --hl-3: var(--light-hl-3); 33 | --hl-4: var(--light-hl-4); 34 | --hl-5: var(--light-hl-5); 35 | --hl-6: var(--light-hl-6); 36 | --hl-7: var(--light-hl-7); 37 | --hl-8: var(--light-hl-8); 38 | --hl-9: var(--light-hl-9); 39 | --hl-10: var(--light-hl-10); 40 | --code-background: var(--light-code-background); 41 | } } 42 | 43 | @media (prefers-color-scheme: dark) { :root { 44 | --hl-0: var(--dark-hl-0); 45 | --hl-1: var(--dark-hl-1); 46 | --hl-2: var(--dark-hl-2); 47 | --hl-3: var(--dark-hl-3); 48 | --hl-4: var(--dark-hl-4); 49 | --hl-5: var(--dark-hl-5); 50 | --hl-6: var(--dark-hl-6); 51 | --hl-7: var(--dark-hl-7); 52 | --hl-8: var(--dark-hl-8); 53 | --hl-9: var(--dark-hl-9); 54 | --hl-10: var(--dark-hl-10); 55 | --code-background: var(--dark-code-background); 56 | } } 57 | 58 | :root[data-theme='light'] { 59 | --hl-0: var(--light-hl-0); 60 | --hl-1: var(--light-hl-1); 61 | --hl-2: var(--light-hl-2); 62 | --hl-3: var(--light-hl-3); 63 | --hl-4: var(--light-hl-4); 64 | --hl-5: var(--light-hl-5); 65 | --hl-6: var(--light-hl-6); 66 | --hl-7: var(--light-hl-7); 67 | --hl-8: var(--light-hl-8); 68 | --hl-9: var(--light-hl-9); 69 | --hl-10: var(--light-hl-10); 70 | --code-background: var(--light-code-background); 71 | } 72 | 73 | :root[data-theme='dark'] { 74 | --hl-0: var(--dark-hl-0); 75 | --hl-1: var(--dark-hl-1); 76 | --hl-2: var(--dark-hl-2); 77 | --hl-3: var(--dark-hl-3); 78 | --hl-4: var(--dark-hl-4); 79 | --hl-5: var(--dark-hl-5); 80 | --hl-6: var(--dark-hl-6); 81 | --hl-7: var(--dark-hl-7); 82 | --hl-8: var(--dark-hl-8); 83 | --hl-9: var(--dark-hl-9); 84 | --hl-10: var(--dark-hl-10); 85 | --code-background: var(--dark-code-background); 86 | } 87 | 88 | .hl-0 { color: var(--hl-0); } 89 | .hl-1 { color: var(--hl-1); } 90 | .hl-2 { color: var(--hl-2); } 91 | .hl-3 { color: var(--hl-3); } 92 | .hl-4 { color: var(--hl-4); } 93 | .hl-5 { color: var(--hl-5); } 94 | .hl-6 { color: var(--hl-6); } 95 | .hl-7 { color: var(--hl-7); } 96 | .hl-8 { color: var(--hl-8); } 97 | .hl-9 { color: var(--hl-9); } 98 | .hl-10 { color: var(--hl-10); } 99 | pre, code { background: var(--code-background); } 100 | -------------------------------------------------------------------------------- /.taskmaster/tasks/task_017.txt: -------------------------------------------------------------------------------- 1 | # Task ID: 17 2 | # Title: Fix failing API tests in packages/api/src/routes/agents.test.ts 3 | # Status: done 4 | # Dependencies: 16 5 | # Priority: medium 6 | # Description: Debug and resolve failing test cases in the agents route test suite to ensure all API endpoints are properly tested and validated. 7 | # Details: 8 | 1. Analyze the current test failures in packages/api/src/routes/agents.test.ts by running the test suite and identifying specific error messages, assertion failures, or timeout issues. 9 | 10 | 2. Update test mocks and fixtures to align with the new SDK integration implemented in Task 16: 11 | - Replace any hardcoded mock data with realistic test data that matches SDK response formats 12 | - Update test assertions to expect actual blockchain data structures instead of mock responses 13 | - Mock the @ensemble/sdk methods properly using jest.mock() or similar testing framework mocking capabilities 14 | 15 | 3. Fix test setup and teardown procedures: 16 | - Ensure proper test database/blockchain state initialization before each test 17 | - Add cleanup procedures to reset state between tests 18 | - Configure test environment variables for testnet connections if needed 19 | 20 | 4. Address authentication and authorization test scenarios: 21 | - Update tests to handle any new authentication requirements 22 | - Mock authentication tokens or user sessions as needed 23 | - Test both authenticated and unauthenticated request scenarios 24 | 25 | 5. Update test assertions for error handling: 26 | - Verify that API endpoints return appropriate HTTP status codes 27 | - Test error response formats match the expected API contract 28 | - Ensure blockchain connection failures are handled gracefully in tests 29 | 30 | 6. Add missing test coverage for any new endpoints or functionality: 31 | - Test all CRUD operations (GET, POST, PUT, DELETE) for agent routes 32 | - Add edge case testing for invalid inputs, malformed requests, and boundary conditions 33 | - Test rate limiting, pagination, and query parameter handling if applicable 34 | 35 | # Test Strategy: 36 | 1. Run the failing test suite to establish baseline failure count and specific error messages: `npm test packages/api/src/routes/agents.test.ts --verbose` 37 | 38 | 2. Fix tests incrementally and verify each fix: 39 | - Run individual test cases to isolate and resolve specific failures 40 | - Use `--watch` mode during development for rapid feedback 41 | - Ensure each test passes consistently across multiple runs 42 | 43 | 3. Validate test coverage and quality: 44 | - Run coverage reports to ensure all code paths in agents routes are tested 45 | - Verify that tests cover both success and failure scenarios 46 | - Check that mocked SDK methods are called with expected parameters 47 | 48 | 4. Integration testing validation: 49 | - Run tests against a test environment with actual SDK integration 50 | - Verify that tests work with both mocked and real SDK responses 51 | - Test with different network conditions and error scenarios 52 | 53 | 5. Regression testing: 54 | - Run the full API test suite to ensure fixes don't break other tests 55 | - Verify that all agent-related API endpoints still function correctly 56 | - Test the API manually using tools like Postman or curl to confirm test accuracy 57 | -------------------------------------------------------------------------------- /.taskmaster/tasks/task_016.txt: -------------------------------------------------------------------------------- 1 | # Task ID: 16 2 | # Title: Implement REST API endpoints by integrating with the Ensemble SDK 3 | # Status: done 4 | # Dependencies: 1, 3 5 | # Priority: medium 6 | # Description: Connect agent routes to actual SDK methods for fetching agent data from blockchain, handle error cases, and ensure proper data transformation between SDK responses and API response formats. 7 | # Details: 8 | 1. Install and configure the @ensemble/sdk package in the REST API project, ensuring proper initialization with Base mainnet and Sepolia testnet configurations. 9 | 10 | 2. Replace all mock agent data endpoints with real SDK integration: 11 | - Update GET /agents endpoint to use sdk.getAgents() method 12 | - Modify GET /agents/:id to use sdk.getAgent(id) for individual agent retrieval 13 | - Connect POST /agents to sdk.registerAgent() for new agent registration 14 | - Update PUT/PATCH /agents/:id to use sdk.updateAgent() methods 15 | - Implement DELETE /agents/:id using sdk.deregisterAgent() 16 | 17 | 3. Implement comprehensive error handling: 18 | - Catch blockchain network errors and return appropriate HTTP status codes (503 for network issues, 404 for not found, 400 for invalid parameters) 19 | - Add retry logic for transient network failures with exponential backoff 20 | - Create standardized error response format with error codes and user-friendly messages 21 | - Handle gas estimation failures and transaction timeout scenarios 22 | 23 | 4. Implement data transformation layer: 24 | - Create mapping functions to convert SDK response objects to API response format 25 | - Ensure consistent field naming and data types across API responses 26 | - Add data validation for incoming requests before passing to SDK methods 27 | - Implement response caching for frequently accessed agent data to reduce blockchain calls 28 | 29 | 5. Add proper async/await handling throughout all endpoints with appropriate error propagation and logging for debugging blockchain interactions. 30 | 31 | # Test Strategy: 32 | 1. Integration testing with testnet deployment: 33 | - Test all CRUD operations against deployed smart contracts on Base Sepolia 34 | - Verify that API responses match actual on-chain agent data 35 | - Test agent registration, updates, and deregistration workflows end-to-end 36 | 37 | 2. Error handling validation: 38 | - Simulate network failures by disconnecting from blockchain nodes 39 | - Test timeout scenarios with long-running transactions 40 | - Verify proper HTTP status codes and error messages for various failure modes 41 | - Test retry logic with intermittent network issues 42 | 43 | 3. Data transformation verification: 44 | - Compare SDK response objects with API response format to ensure proper mapping 45 | - Test edge cases like missing optional fields and null values 46 | - Validate response schema compliance with OpenAPI documentation 47 | 48 | 4. Performance testing: 49 | - Measure response times for blockchain data fetching vs previous mock data 50 | - Test concurrent request handling and rate limiting 51 | - Verify caching effectiveness for repeated agent data requests 52 | 53 | 5. End-to-end workflow testing: 54 | - Test complete agent lifecycle from registration through task execution 55 | - Verify integration with existing authentication and authorization systems 56 | - Test API functionality with real blockchain transactions and gas costs 57 | -------------------------------------------------------------------------------- /packages/python-sdk/src/services/proposal_service.py: -------------------------------------------------------------------------------- 1 | 2 | from google.cloud import pubsub_v1 3 | from typing import Any, Callable, Optional 4 | from web3.contract import Contract 5 | from eth_typing import Address 6 | from ..types import Proposal 7 | 8 | class ProposalService: 9 | def __init__(self, project_id: str, topic_name: str, task_registry: Contract): 10 | self.pubsub = pubsub_v1.PublisherClient() 11 | self.topic_name = topic_name 12 | self.task_registry = task_registry 13 | self.subscription = None 14 | self.on_new_proposal: Optional[Callable[[Proposal], None]] = None 15 | self.project_id = project_id 16 | 17 | async def setup_subscription(self, user_id: str): 18 | subscription_name = f"tasks-{user_id}" 19 | topic_path = self.pubsub.topic_path(self.project_id, self.topic_name) 20 | 21 | # Create topic if it doesn't exist 22 | try: 23 | self.pubsub.get_topic(request={"topic": topic_path}) 24 | except Exception: 25 | self.pubsub.create_topic(request={"name": topic_path}) 26 | 27 | subscription_path = self.pubsub.subscription_path( 28 | self.project_id, subscription_name 29 | ) 30 | 31 | # Create subscription if it doesn't exist 32 | try: 33 | self.pubsub.get_subscription(request={"subscription": subscription_path}) 34 | except Exception: 35 | self.pubsub.create_subscription( 36 | request={"name": subscription_path, "topic": topic_path} 37 | ) 38 | 39 | def callback(message): 40 | if self.on_new_proposal: 41 | proposal = Proposal.from_dict(message.data) 42 | self.on_new_proposal(proposal) 43 | message.ack() 44 | 45 | subscriber = pubsub_v1.SubscriberClient() 46 | self.subscription = subscriber.subscribe(subscription_path, callback) 47 | 48 | async def send_proposal(self, task_id: str, agent_id: str, price: int) -> None: 49 | topic_path = self.pubsub.topic_path(self.project_id, self.topic_name) 50 | data = { 51 | "taskId": task_id, 52 | "price": str(price), 53 | "agent": agent_id 54 | } 55 | 56 | try: 57 | future = self.pubsub.publish( 58 | topic_path, 59 | str(data).encode("utf-8") 60 | ) 61 | future.result() 62 | print(f"Proposal for task {task_id} sent successfully.") 63 | except Exception as e: 64 | print(f"Failed to send proposal for task {task_id}:", e) 65 | raise e 66 | 67 | async def get_proposals(self, task_id: str) -> list[str]: 68 | # Implement PubSub message pulling if needed 69 | return [] 70 | 71 | def set_on_new_proposal_listener(self, listener: Callable[[Proposal], None]): 72 | self.on_new_proposal = listener 73 | 74 | async def approve_proposal(self, task_id: int, proposal: Proposal) -> None: 75 | try: 76 | tx = await self.task_registry.functions.approveProposal( 77 | task_id, 78 | proposal 79 | ).transact() 80 | await tx.wait() 81 | except Exception as e: 82 | print("Approving proposal failed:", e) 83 | raise e 84 | -------------------------------------------------------------------------------- /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | This monorepo uses [changesets](https://github.com/changesets/changesets) for version management and publishing. 4 | 5 | ## Quick Start 6 | 7 | ```bash 8 | # Create a changeset 9 | pnpm changeset 10 | 11 | # Check changeset status 12 | pnpm changeset:status 13 | 14 | # Version packages (usually done by CI) 15 | pnpm changeset:version 16 | 17 | # Publish packages (usually done by CI) 18 | pnpm changeset:publish 19 | ``` 20 | 21 | ## Creating Changesets 22 | 23 | When you make changes that should be released, create a changeset: 24 | 25 | ```bash 26 | pnpm changeset 27 | ``` 28 | 29 | This will: 30 | 1. Ask which packages have changed 31 | 2. Ask what type of change (major/minor/patch) for each 32 | 3. Prompt you to write a summary 33 | 34 | ## Change Types 35 | 36 | - **patch** (0.0.x): Bug fixes, small improvements, documentation updates 37 | - **minor** (0.x.0): New features, new API methods (backwards compatible) 38 | - **major** (x.0.0): Breaking changes, API changes that break existing code 39 | 40 | ## Changeset Categories 41 | 42 | Use these prefixes in your changeset summaries: 43 | 44 | - `feat:` New features 45 | - `fix:` Bug fixes 46 | - `docs:` Documentation changes 47 | - `refactor:` Code refactoring 48 | - `perf:` Performance improvements 49 | - `test:` Test additions/changes 50 | - `chore:` Maintenance tasks 51 | - `breaking:` Breaking changes (auto-triggers major version) 52 | 53 | ## Example Changeset 54 | 55 | ```markdown 56 | --- 57 | "@ensemble-ai/sdk": minor 58 | "@ensemble-ai/cli": patch 59 | --- 60 | 61 | feat: Add subgraphUrl as required parameter to SDK configuration 62 | 63 | - SDK: subgraphUrl is now required in EnsembleConfig 64 | - CLI: Updated to always provide subgraphUrl from config 65 | - This ensures agent query methods always work correctly 66 | ``` 67 | 68 | ## Package Versioning 69 | 70 | Each package versions independently: 71 | 72 | - **@ensemble-ai/sdk**: Core SDK, follows semver strictly 73 | - **@ensemble-ai/cli**: CLI tool, minor bumps for new commands 74 | - **@ensemble-ai/contracts**: Smart contracts, major bumps for contract changes 75 | - **@ensemble-ai/subgraph**: Subgraph indexer, syncs with contract versions 76 | - **@ensemble-ai/mcp-server**: MCP server, independent versioning 77 | 78 | ## Release Process 79 | 80 | ### Automated (Recommended) 81 | 1. Create changeset with your PR 82 | 2. Merge PR to main 83 | 3. CI creates "Release packages" PR 84 | 4. Review and merge release PR 85 | 5. CI publishes to npm 86 | 87 | ### Manual 88 | ```bash 89 | pnpm changeset:version # Update versions and changelogs 90 | pnpm changeset:publish # Publish to npm 91 | ``` 92 | 93 | ### Prerelease 94 | ```bash 95 | # Enter prerelease mode 96 | pnpm changeset pre enter alpha 97 | 98 | # Create changesets normally 99 | pnpm changeset 100 | 101 | # Version and publish with tag 102 | pnpm changeset:version 103 | pnpm changeset:publish --tag alpha 104 | 105 | # Exit prerelease mode 106 | pnpm changeset pre exit 107 | ``` 108 | 109 | ## Troubleshooting 110 | 111 | ### "No changesets present" 112 | You forgot to create a changeset! Run `pnpm changeset`. 113 | 114 | ### Package not updating 115 | Make sure the package is listed in your changeset markdown. 116 | 117 | ### Wrong version bump 118 | Delete the changeset file and recreate it with the correct type. 119 | 120 | ### Internal dependency conflicts 121 | Changesets will automatically update internal dependencies based on the `updateInternalDependencies` setting. -------------------------------------------------------------------------------- /packages/subgraph/tests/task-registry-utils.ts: -------------------------------------------------------------------------------- 1 | import { newMockEvent } from "matchstick-as" 2 | import { ethereum, Address, BigInt } from "@graphprotocol/graph-ts" 3 | import { 4 | OwnershipTransferred, 5 | PermissionUpdated, 6 | TaskAssigned, 7 | TaskCreated, 8 | TaskStatusChanged 9 | } from "../generated/TaskRegistry/TaskRegistry" 10 | 11 | export function createOwnershipTransferredEvent( 12 | previousOwner: Address, 13 | newOwner: Address 14 | ): OwnershipTransferred { 15 | let ownershipTransferredEvent = changetype( 16 | newMockEvent() 17 | ) 18 | 19 | ownershipTransferredEvent.parameters = new Array() 20 | 21 | ownershipTransferredEvent.parameters.push( 22 | new ethereum.EventParam( 23 | "previousOwner", 24 | ethereum.Value.fromAddress(previousOwner) 25 | ) 26 | ) 27 | ownershipTransferredEvent.parameters.push( 28 | new ethereum.EventParam("newOwner", ethereum.Value.fromAddress(newOwner)) 29 | ) 30 | 31 | return ownershipTransferredEvent 32 | } 33 | 34 | export function createPermissionUpdatedEvent( 35 | taskId: BigInt, 36 | user: Address, 37 | allowed: boolean 38 | ): PermissionUpdated { 39 | let permissionUpdatedEvent = changetype(newMockEvent()) 40 | 41 | permissionUpdatedEvent.parameters = new Array() 42 | 43 | permissionUpdatedEvent.parameters.push( 44 | new ethereum.EventParam("taskId", ethereum.Value.fromUnsignedBigInt(taskId)) 45 | ) 46 | permissionUpdatedEvent.parameters.push( 47 | new ethereum.EventParam("user", ethereum.Value.fromAddress(user)) 48 | ) 49 | permissionUpdatedEvent.parameters.push( 50 | new ethereum.EventParam("allowed", ethereum.Value.fromBoolean(allowed)) 51 | ) 52 | 53 | return permissionUpdatedEvent 54 | } 55 | 56 | export function createTaskAssignedEvent( 57 | taskId: BigInt, 58 | agent: Address 59 | ): TaskAssigned { 60 | let taskAssignedEvent = changetype(newMockEvent()) 61 | 62 | taskAssignedEvent.parameters = new Array() 63 | 64 | taskAssignedEvent.parameters.push( 65 | new ethereum.EventParam("taskId", ethereum.Value.fromUnsignedBigInt(taskId)) 66 | ) 67 | taskAssignedEvent.parameters.push( 68 | new ethereum.EventParam("agent", ethereum.Value.fromAddress(agent)) 69 | ) 70 | 71 | return taskAssignedEvent 72 | } 73 | 74 | export function createTaskCreatedEvent( 75 | owner: Address, 76 | taskId: BigInt 77 | ): TaskCreated { 78 | let taskCreatedEvent = changetype(newMockEvent()) 79 | 80 | taskCreatedEvent.parameters = new Array() 81 | 82 | taskCreatedEvent.parameters.push( 83 | new ethereum.EventParam("owner", ethereum.Value.fromAddress(owner)) 84 | ) 85 | taskCreatedEvent.parameters.push( 86 | new ethereum.EventParam("taskId", ethereum.Value.fromUnsignedBigInt(taskId)) 87 | ) 88 | 89 | return taskCreatedEvent 90 | } 91 | 92 | export function createTaskStatusChangedEvent( 93 | taskId: BigInt, 94 | status: i32 95 | ): TaskStatusChanged { 96 | let taskStatusChangedEvent = changetype(newMockEvent()) 97 | 98 | taskStatusChangedEvent.parameters = new Array() 99 | 100 | taskStatusChangedEvent.parameters.push( 101 | new ethereum.EventParam("taskId", ethereum.Value.fromUnsignedBigInt(taskId)) 102 | ) 103 | taskStatusChangedEvent.parameters.push( 104 | new ethereum.EventParam( 105 | "status", 106 | ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(status)) 107 | ) 108 | ) 109 | 110 | return taskStatusChangedEvent 111 | } 112 | -------------------------------------------------------------------------------- /packages/contracts/scripts/deploy.ts: -------------------------------------------------------------------------------- 1 | import hre from "hardhat"; 2 | import { upgrades } from "hardhat"; 3 | 4 | async function main(): Promise { 5 | const [deployer] = await hre.ethers.getSigners(); 6 | 7 | console.log(`Deployer address: ${deployer.address}`); 8 | 9 | // Deploy ServiceRegistryUpgradeable first (no dependencies) 10 | console.log("Deploying ServiceRegistryUpgradeable..."); 11 | const ServiceRegistry = await hre.ethers.getContractFactory("ServiceRegistryUpgradeable"); 12 | const serviceRegistry = await upgrades.deployProxy(ServiceRegistry, [], { 13 | initializer: "initialize", 14 | kind: "uups" 15 | }); 16 | await serviceRegistry.waitForDeployment(); 17 | const serviceRegistryAddress = await serviceRegistry.getAddress(); 18 | console.log(`SERVICE_REGISTRY_ADDRESS=${serviceRegistryAddress}`); 19 | 20 | // For AgentsRegistry, we need a V1 registry address - let's create a mock one 21 | console.log("Deploying mock V1 registry for compatibility..."); 22 | const mockV1Registry = await upgrades.deployProxy(ServiceRegistry, [], { 23 | initializer: "initialize", 24 | kind: "uups" 25 | }); 26 | await mockV1Registry.waitForDeployment(); 27 | const mockV1Address = await mockV1Registry.getAddress(); 28 | 29 | // Deploy AgentsRegistryUpgradeable (depends on ServiceRegistry) 30 | console.log("Deploying AgentsRegistryUpgradeable..."); 31 | const AgentsRegistry = await hre.ethers.getContractFactory("AgentsRegistryUpgradeable"); 32 | const agentsRegistry = await upgrades.deployProxy(AgentsRegistry, [mockV1Address, serviceRegistryAddress], { 33 | initializer: "initialize", 34 | kind: "uups" 35 | }); 36 | await agentsRegistry.waitForDeployment(); 37 | const agentRegistryAddress = await agentsRegistry.getAddress(); 38 | console.log(`AGENT_REGISTRY_ADDRESS=${agentRegistryAddress}`); 39 | 40 | // Deploy TaskRegistryUpgradeable (depends on AgentsRegistry) 41 | console.log("Deploying TaskRegistryUpgradeable..."); 42 | const TaskRegistry = await hre.ethers.getContractFactory("TaskRegistryUpgradeable"); 43 | const taskRegistry = await upgrades.deployProxy(TaskRegistry, [1, agentRegistryAddress], { 44 | initializer: "initialize", 45 | kind: "uups" 46 | }); 47 | await taskRegistry.waitForDeployment(); 48 | const taskRegistryAddress = await taskRegistry.getAddress(); 49 | console.log(`TASK_REGISTRY_ADDRESS=${taskRegistryAddress}`); 50 | 51 | console.log("\n=== Deployment Summary ==="); 52 | console.log(`ServiceRegistry: ${serviceRegistryAddress}`); 53 | console.log(`AgentsRegistry: ${agentRegistryAddress}`); 54 | console.log(`TaskRegistry: ${taskRegistryAddress}`); 55 | console.log(`Mock V1 Registry: ${mockV1Address}`); 56 | 57 | // Uncomment the lines below if you want to create tasks for testing 58 | /* 59 | const tx1 = await taskRegistry.createTask("Do X for me", 0); 60 | console.log(`First task created in tx: ${tx1.hash}`); 61 | 62 | const tx2 = await taskRegistry.createTask("Do Y for me", 0); 63 | console.log(`Second task created in tx: ${tx2.hash}`); 64 | 65 | const tasks = await taskRegistry.getTasksByOwner(deployer.address); 66 | console.log("Tasks created by deployer:", tasks); 67 | */ 68 | } 69 | 70 | // Error handling and process exit 71 | main() 72 | .then(() => { 73 | console.log("Deployment completed successfully!"); 74 | process.exit(0); 75 | }) 76 | .catch((error) => { 77 | console.error("Error during deployment:", error); 78 | process.exit(1); 79 | }); 80 | -------------------------------------------------------------------------------- /packages/cli/src/types/wallet.ts: -------------------------------------------------------------------------------- 1 | export interface WalletData { 2 | name: string; 3 | address: string; 4 | encrypted: boolean; 5 | createdAt: Date; 6 | type: 'mnemonic' | 'private-key' | 'keystore'; 7 | } 8 | 9 | export interface EncryptedWallet { 10 | name: string; 11 | address: string; 12 | encryptedData: string; 13 | salt: string; 14 | iv: string; 15 | type: 'mnemonic' | 'private-key' | 'keystore'; 16 | createdAt: string; 17 | version: string; 18 | } 19 | 20 | export interface WalletCreateOptions { 21 | name: string; 22 | password: string; 23 | type?: 'mnemonic' | 'private-key'; 24 | } 25 | 26 | export interface WalletImportOptions { 27 | name: string; 28 | password: string; 29 | mnemonic?: string; 30 | privateKey?: string; 31 | keystore?: string; 32 | keystorePassword?: string; 33 | } 34 | 35 | export interface WalletExportOptions { 36 | name: string; 37 | password: string; 38 | format: 'mnemonic' | 'private-key' | 'keystore'; 39 | outputPassword?: string; // For keystore format 40 | } 41 | 42 | export interface WalletBalance { 43 | address: string; 44 | eth: string; 45 | tokens: TokenBalance[]; 46 | } 47 | 48 | export interface TokenBalance { 49 | symbol: string; 50 | name: string; 51 | address: string; 52 | balance: string; 53 | decimals: number; 54 | } 55 | 56 | export interface Transaction { 57 | hash: string; 58 | from: string; 59 | to: string; 60 | value: string; 61 | gasPrice: string; 62 | gasUsed: string; 63 | timestamp: number; 64 | status: 'success' | 'failed' | 'pending'; 65 | blockNumber: number; 66 | } 67 | 68 | export interface WalletManager { 69 | createWallet(options: WalletCreateOptions): Promise<{ address: string; mnemonic?: string }>; 70 | importWallet(options: WalletImportOptions): Promise<{ address: string }>; 71 | listWallets(): Promise; 72 | exportWallet(options: WalletExportOptions): Promise; 73 | deleteWallet(name: string, password: string): Promise; 74 | getWalletAddress(name: string): Promise; 75 | getWalletSigner(name: string, password: string): Promise; 76 | getBalance(nameOrAddress: string): Promise; 77 | getTransactionHistory(nameOrAddress: string, limit?: number): Promise; 78 | } 79 | 80 | export class WalletError extends Error { 81 | constructor(message: string, public readonly code?: string, public readonly cause?: any) { 82 | super(message); 83 | this.name = 'WalletError'; 84 | } 85 | } 86 | 87 | export class WalletNotFoundError extends WalletError { 88 | constructor(walletName: string) { 89 | super(`Wallet '${walletName}' not found`, 'WALLET_NOT_FOUND'); 90 | this.name = 'WalletNotFoundError'; 91 | } 92 | } 93 | 94 | export class WalletAlreadyExistsError extends WalletError { 95 | constructor(walletName: string) { 96 | super(`Wallet '${walletName}' already exists`, 'WALLET_EXISTS'); 97 | this.name = 'WalletAlreadyExistsError'; 98 | } 99 | } 100 | 101 | export class InvalidPasswordError extends WalletError { 102 | constructor() { 103 | super('Invalid password', 'INVALID_PASSWORD'); 104 | this.name = 'InvalidPasswordError'; 105 | } 106 | } 107 | 108 | export class InvalidMnemonicError extends WalletError { 109 | constructor() { 110 | super('Invalid mnemonic phrase', 'INVALID_MNEMONIC'); 111 | this.name = 'InvalidMnemonicError'; 112 | } 113 | } 114 | 115 | export class InvalidPrivateKeyError extends WalletError { 116 | constructor() { 117 | super('Invalid private key', 'INVALID_PRIVATE_KEY'); 118 | this.name = 'InvalidPrivateKeyError'; 119 | } 120 | } -------------------------------------------------------------------------------- /packages/contracts/contracts/ServiceRegistryUpgradeable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.22; 3 | 4 | import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; 5 | import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; 6 | import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; 7 | 8 | /** 9 | * @title ServiceRegistryUpgradeable 10 | * @author leonprou 11 | * @notice A smart contract that stores information about the services provided by agents. 12 | * @dev Upgradeable version using UUPS proxy pattern 13 | */ 14 | contract ServiceRegistryUpgradeable is Initializable, OwnableUpgradeable, UUPSUpgradeable { 15 | struct Service { 16 | string name; 17 | string category; 18 | string description; 19 | } 20 | 21 | mapping(string => Service) public services; 22 | uint256 public serviceCount; 23 | 24 | event ServiceRegistered(string name, string category, string description); 25 | event ServiceUpdated(string name, string category, string description); 26 | 27 | /// @custom:oz-upgrades-unsafe-allow constructor 28 | constructor() { 29 | _disableInitializers(); 30 | } 31 | 32 | /** 33 | * @dev Initializes the contract 34 | */ 35 | function initialize() public initializer { 36 | __Ownable_init(msg.sender); 37 | __UUPSUpgradeable_init(); 38 | serviceCount = 0; 39 | } 40 | 41 | /** 42 | * @dev Registers a new service with the given name and price. 43 | * @param name The name of the service. 44 | * @return The ID of the registered service. 45 | */ 46 | function registerService(string memory name, string memory category, string memory description) external returns (Service memory) { 47 | require(!this.isServiceRegistered(name), "Service already registered"); 48 | 49 | Service memory service = Service({ 50 | name: name, 51 | category: category, 52 | description: description 53 | }); 54 | 55 | services[name] = service; 56 | 57 | emit ServiceRegistered(name, category, description); 58 | 59 | serviceCount++; 60 | return service; 61 | } 62 | 63 | /** 64 | * @dev Retrieves the details of a service. 65 | * @param name The name of the service to retrieve. 66 | * @return The details of the service. 67 | */ 68 | function getService(string memory name) external view returns (Service memory) { 69 | return services[name]; 70 | } 71 | 72 | function isServiceRegistered(string memory name) external view returns (bool) { 73 | require(bytes(name).length > 0, "Invalid service name"); 74 | 75 | return bytes(services[name].name).length > 0; 76 | } 77 | 78 | function updateService(string memory name, string memory category, string memory description) external onlyOwner { 79 | require(this.isServiceRegistered(name), "Service not registered"); 80 | 81 | services[name] = Service({ 82 | name: name, 83 | category: category, 84 | description: description 85 | }); 86 | 87 | emit ServiceUpdated(name, category, description); 88 | } 89 | 90 | /** 91 | * @dev Function that should revert when `msg.sender` is not authorized to upgrade the contract. 92 | * @param newImplementation Address of the new implementation 93 | */ 94 | function _authorizeUpgrade(address newImplementation) internal override onlyOwner {} 95 | 96 | /** 97 | * @dev Storage gap for future upgrades 98 | */ 99 | uint256[50] private __gap; 100 | } -------------------------------------------------------------------------------- /packages/cli/src/commands/agents/list.ts: -------------------------------------------------------------------------------- 1 | import { Command } from 'commander'; 2 | import chalk from 'chalk'; 3 | import { createSDKInstance } from '../../utils/sdk'; 4 | import { formatOutput } from '../../utils/formatters'; 5 | import { saveAgentRecords } from '../../utils/file-operations'; 6 | import { AgentFilterParams } from '@ensemble-ai/sdk'; 7 | 8 | export const listAgentsCommand = new Command('list') 9 | .description('List and discover agents with advanced filtering') 10 | .option('-h, --help', 'Display help information') 11 | .option('--category ', 'Filter by agent category') 12 | .option('--owner
', 'Filter by owner address') 13 | .option('--status ', 'Filter by agent status (active, inactive, maintenance)') 14 | .option('--reputation-min ', 'Filter by minimum reputation score', parseFloat) 15 | .option('--reputation-max ', 'Filter by maximum reputation score', parseFloat) 16 | .option('--name ', 'Search by agent name (case-insensitive)') 17 | .option('--attributes ', 'Filter by attributes/tags (comma-separated)') 18 | .option('--first ', 'Limit number of results (default: 10)', parseInt, 10) 19 | .option('--skip ', 'Skip number of results for pagination (default: 0)', parseInt, 0) 20 | .option('--sort-by ', 'Sort by field (reputation, name, created, updated)', 'reputation') 21 | .option('--sort-order ', 'Sort order (asc, desc) (default: desc)', 'desc') 22 | .option('--format ', 'Output format (table, json, csv, yaml)', 'table') 23 | .option('--include-metadata', 'Include full metadata in output') 24 | .option('--save-records ', 'Save each agent as agent-record.yaml file in specified directory') 25 | .option('--save-records-prefix ', 'Prefix for saved agent-record files (default: agent-record)', 'agent-record') 26 | .action(async (options) => { 27 | if (options.help) { 28 | listAgentsCommand.outputHelp(); 29 | return; 30 | } 31 | 32 | try { 33 | const sdk = await createSDKInstance(); 34 | const agentService = sdk.agents; 35 | 36 | // Build filter parameters 37 | const filters: AgentFilterParams = { 38 | first: options.first, 39 | skip: options.skip 40 | }; 41 | 42 | if (options.category) filters.category = options.category; 43 | if (options.owner) filters.owner = options.owner; 44 | if (options.name) filters.name = options.name; 45 | if (options.reputationMin !== undefined) filters.reputation_min = options.reputationMin; 46 | if (options.reputationMax !== undefined) filters.reputation_max = options.reputationMax; 47 | 48 | console.log(chalk.blue('🔍 Fetching agents...')); 49 | 50 | const agents = await agentService.getAgentRecords(filters); 51 | 52 | if (agents.length === 0) { 53 | console.log(chalk.yellow('No agents found matching the criteria.')); 54 | return; 55 | } 56 | 57 | console.log(chalk.green(`✅ Found ${agents.length} agent(s)`)); 58 | 59 | // Format and display output 60 | const output = formatOutput(agents, options.format, options.includeMetadata); 61 | console.log(output); 62 | 63 | // Save records if requested 64 | if (options.saveRecords) { 65 | await saveAgentRecords(agents, options.saveRecords, options.saveRecordsPrefix); 66 | console.log(chalk.green(`💾 Saved ${agents.length} agent records to ${options.saveRecords}`)); 67 | } 68 | 69 | } catch (error: any) { 70 | console.error(chalk.red('❌ Error fetching agents:')); 71 | console.error(chalk.red(error.message)); 72 | if (options.verbose) { 73 | console.error(error.stack); 74 | } 75 | process.exit(1); 76 | } 77 | }); -------------------------------------------------------------------------------- /packages/mcp-server/tests/agent-matcher.test.ts: -------------------------------------------------------------------------------- 1 | import { AgentMatcher } from '../src/services/agent-matcher'; 2 | import { describe, it, expect, beforeEach } from '@jest/globals'; 3 | 4 | describe('AgentMatcher', () => { 5 | let agentMatcher: AgentMatcher; 6 | 7 | beforeEach(() => { 8 | agentMatcher = new AgentMatcher(); 9 | }); 10 | 11 | describe('findMatchingAgents', () => { 12 | it('should find agents related to AI services', async () => { 13 | const results = await agentMatcher.findMatchingAgents('AI assistant', { limit: 5 }); 14 | console.log('Results:', results); 15 | // Verify we got some results 16 | expect(results.length).toBeGreaterThan(0); 17 | 18 | // Check that each result has the expected properties 19 | results.forEach(result => { 20 | expect(result.id).toBeDefined(); 21 | expect(result.name).toBeDefined(); 22 | expect(result.description).toBeDefined(); 23 | expect(result.score).toBeGreaterThan(0); 24 | expect(result.reputation).toBeDefined(); 25 | }); 26 | }); 27 | 28 | it('should filter agents by minimum reputation score', async () => { 29 | const minScore = 1; 30 | const results = await agentMatcher.findMatchingAgents('assistant', { 31 | limit: 5, 32 | minReputationScore: minScore 33 | }); 34 | 35 | // Skip test if no results 36 | if (results.length === 0) { 37 | console.log('No results found for reputation score test'); 38 | return; 39 | } 40 | 41 | // Verify all returned agents meet the minimum reputation requirement 42 | results.forEach(result => { 43 | expect(parseInt(result.reputation)).toBeGreaterThanOrEqual(minScore); 44 | }); 45 | }); 46 | 47 | it('should respect the limit parameter', async () => { 48 | const limit = 3; 49 | const results = await agentMatcher.findMatchingAgents('data', { limit }); 50 | 51 | // The number of results should be less than or equal to the limit 52 | expect(results.length).toBeLessThanOrEqual(limit); 53 | }); 54 | 55 | it('should return agents sorted by score', async () => { 56 | const results = await agentMatcher.findMatchingAgents('translation', { limit: 5 }); 57 | 58 | // If we have at least 2 results, verify they're sorted by score (highest first) 59 | if (results.length >= 2) { 60 | for (let i = 0; i < results.length - 1; i++) { 61 | expect(results[i].score).toBeGreaterThanOrEqual(results[i+1].score); 62 | } 63 | } 64 | }); 65 | }); 66 | 67 | describe('getAgentDetails', () => { 68 | it('should return details for a valid agent ID', async () => { 69 | // To test this meaningfully, we first need to get a valid agent ID from the subgraph 70 | const agents = await agentMatcher.findMatchingAgents('agent', { limit: 1 }); 71 | 72 | // Skip test if no agents found 73 | if (agents.length === 0) { 74 | console.log('No agents found to test getAgentDetails'); 75 | return; 76 | } 77 | 78 | const agentId = agents[0].id; 79 | const agentDetails = await agentMatcher.getAgentDetails(agentId); 80 | 81 | // Verify the agent details 82 | expect(agentDetails).not.toBeNull(); 83 | if (agentDetails) { 84 | expect(agentDetails.id).toBe(agentId); 85 | expect(agentDetails.name).toBeDefined(); 86 | expect(agentDetails.agentUri).toBeDefined(); 87 | expect(agentDetails.reputation).toBeDefined(); 88 | } 89 | }); 90 | 91 | it('should return null for a non-existent agent ID', async () => { 92 | const nonExistentId = 'non-existent-agent-id-123456789'; 93 | const result = await agentMatcher.getAgentDetails(nonExistentId); 94 | expect(result).toBeNull(); 95 | }); 96 | }); 97 | }); -------------------------------------------------------------------------------- /packages/cli/src/config/manager.ts: -------------------------------------------------------------------------------- 1 | import { readFile, writeFile, mkdir } from 'fs/promises'; 2 | import { existsSync } from 'fs'; 3 | import { join } from 'path'; 4 | import { homedir } from 'os'; 5 | import { CLIConfig } from '../types/config'; 6 | 7 | const CONFIG_DIR = join(homedir(), '.ensemble'); 8 | const CONFIG_FILE = join(CONFIG_DIR, 'config.json'); 9 | 10 | const DEFAULT_CONFIG: CLIConfig = { 11 | network: 'baseSepolia', 12 | rpcUrl: 'https://base-sepolia.g.alchemy.com/v2/-KE1qv6R383LymGX4KJpSWZrEgVfuW_7', 13 | gasPrice: '20', 14 | outputFormat: 'yaml', 15 | contracts: { 16 | agentRegistry: '0xDbF645cC23066cc364C4Db915c78135eE52f11B2', 17 | taskRegistry: '0x847fA49b999489fD2780fe2843A7b1608106b49b', 18 | serviceRegistry: '0x3Acbf1Ca047a18bE88E7160738A9B0bB64203244' 19 | }, 20 | subgraphUrl: 'https://api.goldsky.com/api/public/project_cmcnps2k01akp01uobifl4bby/subgraphs/ensemble-subgraph/0.0.5/gn', 21 | pinata: { 22 | jwt: undefined, 23 | gateway: undefined 24 | } 25 | }; 26 | 27 | export async function getConfig(): Promise { 28 | try { 29 | if (!existsSync(CONFIG_FILE)) { 30 | await saveConfig(DEFAULT_CONFIG); 31 | return DEFAULT_CONFIG; 32 | } 33 | 34 | const configData = await readFile(CONFIG_FILE, 'utf-8'); 35 | const config = JSON.parse(configData); 36 | 37 | // Merge with defaults to ensure all fields are present 38 | return { ...DEFAULT_CONFIG, ...config }; 39 | } catch (error) { 40 | console.warn('Error reading config, using defaults:', error); 41 | return DEFAULT_CONFIG; 42 | } 43 | } 44 | 45 | export async function saveConfig(config: CLIConfig): Promise { 46 | try { 47 | // Ensure config directory exists 48 | if (!existsSync(CONFIG_DIR)) { 49 | await mkdir(CONFIG_DIR, { recursive: true }); 50 | } 51 | 52 | await writeFile(CONFIG_FILE, JSON.stringify(config, null, 2)); 53 | } catch (error) { 54 | throw new Error(`Failed to save config: ${error}`); 55 | } 56 | } 57 | 58 | export async function updateConfig(updates: Partial): Promise { 59 | const currentConfig = await getConfig(); 60 | const newConfig = { ...currentConfig, ...updates }; 61 | await saveConfig(newConfig); 62 | return newConfig; 63 | } 64 | 65 | export async function resetConfig(): Promise { 66 | await saveConfig(DEFAULT_CONFIG); 67 | return DEFAULT_CONFIG; 68 | } 69 | 70 | // Active wallet management 71 | export async function setActiveWallet(walletName: string): Promise { 72 | const currentConfig = await getConfig(); 73 | const newConfig = { ...currentConfig, activeWallet: walletName }; 74 | await saveConfig(newConfig); 75 | return newConfig; 76 | } 77 | 78 | export async function getActiveWallet(): Promise { 79 | const config = await getConfig(); 80 | return config.activeWallet; 81 | } 82 | 83 | export async function clearActiveWallet(): Promise { 84 | const currentConfig = await getConfig(); 85 | const newConfig = { ...currentConfig }; 86 | delete newConfig.activeWallet; 87 | await saveConfig(newConfig); 88 | return newConfig; 89 | } 90 | 91 | // Environment variable overrides 92 | export function getConfigWithEnvOverrides(): Promise { 93 | return getConfig().then(config => ({ 94 | ...config, 95 | network: (process.env.ENSEMBLE_NETWORK as 'mainnet' | 'sepolia' | 'baseSepolia') || config.network, 96 | rpcUrl: process.env.ENSEMBLE_RPC_URL || config.rpcUrl, 97 | privateKey: process.env.ENSEMBLE_PRIVATE_KEY || config.privateKey, 98 | gasPrice: process.env.ENSEMBLE_GAS_PRICE || config.gasPrice, 99 | outputFormat: (process.env.ENSEMBLE_OUTPUT_FORMAT as 'table' | 'json' | 'csv' | 'yaml') || config.outputFormat, 100 | activeWallet: process.env.ENSEMBLE_ACTIVE_WALLET || config.activeWallet 101 | })); 102 | } -------------------------------------------------------------------------------- /packages/contracts/hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import { HardhatUserConfig } from "hardhat/config"; 2 | import "@nomicfoundation/hardhat-toolbox"; 3 | import "@nomicfoundation/hardhat-ignition-ethers"; 4 | import "@nomicfoundation/hardhat-verify"; 5 | import "@openzeppelin/hardhat-upgrades"; 6 | import * as dotenv from "dotenv"; 7 | import 'solidity-coverage'; 8 | 9 | dotenv.config(); 10 | 11 | // Explicitly disable telemetry 12 | process.env.HARDHAT_TELEMETRY_DISABLED = "1"; 13 | 14 | const config: HardhatUserConfig = { 15 | solidity: { 16 | compilers: [ 17 | { 18 | version: "0.8.20", 19 | settings: { 20 | optimizer: { 21 | enabled: true, 22 | runs: 200 23 | } 24 | } 25 | }, 26 | { 27 | version: "0.8.22", 28 | settings: { 29 | optimizer: { 30 | enabled: true, 31 | runs: 200 32 | } 33 | } 34 | } 35 | ] 36 | }, 37 | networks: { 38 | hardhat: { 39 | chainId: 31337 40 | }, 41 | local: { 42 | url: "http://127.0.0.1:8545/", 43 | chainId: 31337 44 | }, 45 | base: { 46 | url: process.env.BASE_MAINNET_RPC_URL || "https://mainnet.base.org", 47 | chainId: 8453, 48 | accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [], 49 | }, 50 | baseSepolia: { 51 | url: process.env.BASE_SEPOLIA_RPC_URL || "https://sepolia.base.org", 52 | chainId: 84532, 53 | accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [], 54 | }, 55 | neondevnet: { 56 | url: "https://devnet.neonevm.org", 57 | accounts: [ process.env.PRIVATE_KEY! ], 58 | chainId: 245022926, 59 | allowUnlimitedContractSize: false, 60 | gas: "auto", 61 | gasPrice: "auto", 62 | }, 63 | somniaTestnet: { 64 | url: "https://dream-rpc.somnia.network", 65 | accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [], 66 | chainId: 50312, 67 | gasMultiplier: 1.2 68 | // allowUnlimitedContractSize: false, 69 | // gas: "auto", 70 | // gasPrice: "auto", 71 | } 72 | }, 73 | paths: { 74 | sources: "./contracts", 75 | tests: "./test", 76 | cache: "./cache", 77 | artifacts: "./artifacts" 78 | }, 79 | mocha: { 80 | timeout: 40000 81 | }, 82 | etherscan: { 83 | apiKey: { 84 | base: process.env.BASESCAN_API_KEY || "", 85 | baseSepolia: process.env.BASESCAN_API_KEY || "", 86 | }, 87 | customChains: [ 88 | { 89 | network: "neonevm", 90 | chainId: 245022926, 91 | urls: { 92 | apiURL: "https://devnet-api.neonscan.org/hardhat/verify", 93 | browserURL: "https://devnet.neonscan.org" 94 | } 95 | }, 96 | { 97 | network: "baseSepolia", 98 | chainId: 84532, 99 | urls: { 100 | apiURL: "https://api-sepolia.basescan.org/api", 101 | browserURL: "https://sepolia.basescan.org" 102 | } 103 | }, 104 | { 105 | network: "somniaTestnet", 106 | chainId: 50312, 107 | urls: { 108 | apiURL: "https://dream-rpc.somnia.network", 109 | browserURL: "https://shannon-explorer.somnia.network/" 110 | } 111 | } 112 | ] 113 | }, 114 | sourcify: { 115 | enabled: true 116 | } 117 | }; 118 | 119 | export default config; -------------------------------------------------------------------------------- /packages/cli/src/commands/config.ts: -------------------------------------------------------------------------------- 1 | import { Command } from 'commander'; 2 | import chalk from 'chalk'; 3 | import { getConfig, updateConfig, resetConfig } from '../config/manager'; 4 | import { formatOutput } from '../utils/formatters'; 5 | 6 | export const configCommand = new Command('config') 7 | .description('Manage CLI configuration and network settings'); 8 | 9 | configCommand 10 | .command('show') 11 | .description('Display current configuration') 12 | .option('--format ', 'Output format (table, json, yaml)', 'table') 13 | .action(async (options) => { 14 | try { 15 | const config = await getConfig(); 16 | 17 | // Remove sensitive information for display 18 | const displayConfig = { ...config }; 19 | if (displayConfig.privateKey) { 20 | displayConfig.privateKey = '***HIDDEN***'; 21 | } 22 | 23 | const output = formatOutput([displayConfig], options.format); 24 | console.log(output); 25 | } catch (error: any) { 26 | console.error(chalk.red('❌ Error reading configuration:')); 27 | console.error(chalk.red(error.message)); 28 | process.exit(1); 29 | } 30 | }); 31 | 32 | configCommand 33 | .command('set-network ') 34 | .description('Set default network (mainnet, sepolia)') 35 | .action(async (network: 'mainnet' | 'sepolia') => { 36 | try { 37 | if (!['mainnet', 'sepolia'].includes(network)) { 38 | console.error(chalk.red('❌ Invalid network. Use: mainnet or sepolia')); 39 | process.exit(1); 40 | } 41 | 42 | const rpcUrl = network === 'mainnet' 43 | ? 'https://mainnet.base.org' 44 | : 'https://sepolia.base.org'; 45 | 46 | await updateConfig({ network, rpcUrl }); 47 | console.log(chalk.green(`✅ Network set to ${network}`)); 48 | } catch (error: any) { 49 | console.error(chalk.red('❌ Error updating configuration:')); 50 | console.error(chalk.red(error.message)); 51 | process.exit(1); 52 | } 53 | }); 54 | 55 | configCommand 56 | .command('set-rpc ') 57 | .description('Set custom RPC endpoint') 58 | .action(async (url: string) => { 59 | try { 60 | await updateConfig({ rpcUrl: url }); 61 | console.log(chalk.green(`✅ RPC URL set to ${url}`)); 62 | } catch (error: any) { 63 | console.error(chalk.red('❌ Error updating configuration:')); 64 | console.error(chalk.red(error.message)); 65 | process.exit(1); 66 | } 67 | }); 68 | 69 | configCommand 70 | .command('set-private-key ') 71 | .description('Set default private key (stored securely)') 72 | .action(async (key: string) => { 73 | try { 74 | await updateConfig({ privateKey: key }); 75 | console.log(chalk.green('✅ Private key set successfully')); 76 | } catch (error: any) { 77 | console.error(chalk.red('❌ Error updating configuration:')); 78 | console.error(chalk.red(error.message)); 79 | process.exit(1); 80 | } 81 | }); 82 | 83 | configCommand 84 | .command('set-gas-price ') 85 | .description('Set default gas price (gwei)') 86 | .action(async (price: string) => { 87 | try { 88 | await updateConfig({ gasPrice: price }); 89 | console.log(chalk.green(`✅ Gas price set to ${price} gwei`)); 90 | } catch (error: any) { 91 | console.error(chalk.red('❌ Error updating configuration:')); 92 | console.error(chalk.red(error.message)); 93 | process.exit(1); 94 | } 95 | }); 96 | 97 | configCommand 98 | .command('reset') 99 | .description('Reset to default configuration') 100 | .action(async () => { 101 | try { 102 | await resetConfig(); 103 | console.log(chalk.green('✅ Configuration reset to defaults')); 104 | } catch (error: any) { 105 | console.error(chalk.red('❌ Error resetting configuration:')); 106 | console.error(chalk.red(error.message)); 107 | process.exit(1); 108 | } 109 | }); -------------------------------------------------------------------------------- /.taskmaster/tasks/task_005.txt: -------------------------------------------------------------------------------- 1 | # Task ID: 5 2 | # Title: Develop REST API Layer 3 | # Status: done 4 | # Dependencies: 3 5 | # Priority: low 6 | # Description: This task is deferred until after core functionality is complete. The REST API layer will provide an HTTP-based API abstracting blockchain complexity, with authentication, real-time updates, and comprehensive endpoint coverage for agent discovery and management using the AgentRecord data model. 7 | # Details: 8 | Development of the Fastify API server and agent-focused endpoints is postponed until all core system features are delivered. When resumed, the implementation will follow the agent-api-endpoints.md specification, including endpoints for agent discovery, management, and metadata retrieval. The API will use the AgentRecord data model and incorporate Fastify-specific features such as plugins for authentication (JWT and API key management), JSON Schema validation, request/response lifecycle hooks, decorators for dependency injection, rate limiting, and comprehensive error handling. Deployment will utilize Docker containers and environment-based configuration. The focus will remain on robust agent discovery mechanisms and efficient agent data retrieval leveraging Fastify's high-performance architecture. 9 | 10 | # Test Strategy: 11 | Testing for the REST API layer will commence after core functionality is complete. Planned tests include API testing with Postman/Jest for all agent endpoints, load testing for concurrent agent queries, authentication and authorization testing with Fastify plugins, performance testing for agent discovery queries, Fastify-specific testing for plugin integration and hook execution, and JSON Schema validation for all request/response payloads using the AgentRecord model. 12 | 13 | # Subtasks: 14 | ## 1. Set Up Fastify API Server and Agent Endpoints [done] 15 | ### Dependencies: None 16 | ### Description: Initialize the Fastify server and implement REST endpoints for agent management following agent-api-endpoints.md specification, using AgentRecord data model for all responses. 17 | ### Details: 18 | Create a Fastify project structure. Define routes for GET /agents (with filtering/search), GET /agents/{agentId}, POST /agents/discovery, GET /agents/owner/{ownerAddress}, GET /agents/categories, and GET /agents/skills. Implement AgentRecord data model exactly as specified in documentation. Ensure modular route organization for scalability. 19 | 20 | Successfully implemented Fastify API server with all required agent endpoints. Server includes: 21 | 22 | 1. Complete Fastify server setup with middleware (CORS, rate limiting, JWT auth, error handling) 23 | 2. Full AgentRecord data model matching specification 24 | 3. Modular route structure in src/routes/agents.ts 25 | 4. All required REST endpoints: 26 | - GET /api/v1/agents (list with filtering/pagination) 27 | - GET /api/v1/agents/{agentId} (agent details) 28 | - POST /api/v1/agents/discovery (advanced discovery) 29 | - GET /api/v1/agents/owner/{ownerAddress} (agents by owner) 30 | - GET /api/v1/agents/categories (available categories) 31 | - GET /api/v1/agents/skills (available skills) 32 | 33 | 5. Complete TypeScript types and interfaces 34 | 6. Mock data service layer for development 35 | 7. Comprehensive request validation with JSON schemas 36 | 8. Proper error handling and logging 37 | 9. Health check endpoint at /health 38 | 39 | Build and type checking pass successfully. Ready for testing and deployment. 40 | 41 | 42 | ## 3. Add Request Validation, Error Handling, and Middleware [done] 43 | ### Dependencies: 5.1 44 | ### Description: Apply JSON Schema validation to all request bodies and query parameters for agent endpoints. Implement Fastify hooks for request/response lifecycle management and custom error schemas for consistent error responses. 45 | ### Details: 46 | Define JSON Schemas for each agent endpoint including AgentRecord response schema. Use Fastify's built-in validation and hooks (onRequest, preHandler, onSend) for middleware logic. Create custom error handlers for unified error formatting. 47 | 48 | -------------------------------------------------------------------------------- /packages/python-sdk/src/ensemble.py: -------------------------------------------------------------------------------- 1 | 2 | from web3 import Web3 3 | from eth_typing import Address 4 | from .services.agent_service import AgentService 5 | from .services.task_service import TaskService 6 | from .services.proposal_service import ProposalService 7 | from .services.contract_service import ContractService 8 | from .types import ContractConfig, Proposal 9 | from typing import Callable, Optional 10 | 11 | class Ensemble: 12 | def __init__(self, config: ContractConfig): 13 | self.contract_service = ContractService( 14 | Web3(Web3.HTTPProvider(config.network.rpc_url)), 15 | None # Account will be set in connect() 16 | ) 17 | 18 | # Load contract ABIs 19 | with open('src/abi/AgentsRegistry.abi.json') as f: 20 | agents_registry_abi = f.read() 21 | with open('src/abi/TaskRegistry.abi.json') as f: 22 | task_registry_abi = f.read() 23 | 24 | self.agent_registry = self.contract_service.create_contract( 25 | config.agent_registry_address, 26 | agents_registry_abi 27 | ) 28 | self.task_registry = self.contract_service.create_contract( 29 | config.task_registry_address, 30 | task_registry_abi 31 | ) 32 | 33 | self.config = config 34 | 35 | async def connect(self, private_key: str): 36 | account = self.contract_service.provider.eth.account.from_key(private_key) 37 | self.contract_service.account = account 38 | 39 | # Initialize services 40 | self.agent_service = AgentService(self.agent_registry, account) 41 | self.task_service = TaskService(self.task_registry, account) 42 | self.proposal_service = ProposalService( 43 | project_id='ensemble-ai-443111', 44 | topic_name='ensemble-tasks', 45 | task_registry=self.task_registry 46 | ) 47 | 48 | await self.contract_service.validate_network(self.config.network.chain_id) 49 | return self 50 | 51 | async def start(self): 52 | user_id = await self.agent_service.get_address() 53 | await self.proposal_service.setup_subscription(user_id) 54 | 55 | async def stop(self): 56 | if self.proposal_service.subscription: 57 | self.proposal_service.subscription.close() 58 | 59 | # Agent Management 60 | async def register_agent(self, model: str, prompt: str, skills: list[str]) -> str: 61 | return await self.agent_service.register_agent(model, prompt, skills) 62 | 63 | async def get_agent_data(self, agent_id: str): 64 | return await self.agent_service.get_agent_data(agent_id) 65 | 66 | async def is_agent_registered(self, agent_id: str) -> bool: 67 | return await self.agent_service.is_agent_registered(agent_id) 68 | 69 | # Task Management 70 | async def create_task(self, params: dict) -> int: 71 | return await self.task_service.create_task(params) 72 | 73 | async def get_task_data(self, task_id: str) -> dict: 74 | return await self.task_service.get_task_data(task_id) 75 | 76 | async def get_tasks_by_owner(self, owner: str) -> list: 77 | return await self.task_service.get_tasks_by_owner(owner) 78 | 79 | async def complete_task(self, task_id: int, result: str) -> None: 80 | await self.task_service.complete_task(task_id, result) 81 | 82 | # Proposal Management 83 | async def send_proposal(self, task_id: str, agent_id: str, price: int) -> None: 84 | await self.proposal_service.send_proposal(task_id, agent_id, price) 85 | 86 | async def get_proposals(self, task_id: str) -> list[str]: 87 | return await self.proposal_service.get_proposals(task_id) 88 | 89 | async def approve_proposal(self, task_id: int, proposal: Proposal) -> None: 90 | await self.proposal_service.approve_proposal(task_id, proposal) 91 | 92 | def set_on_new_proposal_listener(self, listener: Callable[[Proposal], None]): 93 | self.proposal_service.set_on_new_proposal_listener(listener) 94 | 95 | async def get_wallet_address(self) -> str: 96 | return await self.agent_service.get_address() 97 | -------------------------------------------------------------------------------- /packages/contracts/ignition/deployments/chain-84532/artifacts/TaskRegistryUpgradeableModule#TaskRegistryProxy.json: -------------------------------------------------------------------------------- 1 | { 2 | "_format": "hh-sol-artifact-1", 3 | "contractName": "ERC1967Proxy", 4 | "sourceName": "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol", 5 | "abi": [ 6 | { 7 | "inputs": [ 8 | { 9 | "internalType": "address", 10 | "name": "implementation", 11 | "type": "address" 12 | }, 13 | { 14 | "internalType": "bytes", 15 | "name": "_data", 16 | "type": "bytes" 17 | } 18 | ], 19 | "stateMutability": "payable", 20 | "type": "constructor" 21 | }, 22 | { 23 | "inputs": [ 24 | { 25 | "internalType": "address", 26 | "name": "target", 27 | "type": "address" 28 | } 29 | ], 30 | "name": "AddressEmptyCode", 31 | "type": "error" 32 | }, 33 | { 34 | "inputs": [ 35 | { 36 | "internalType": "address", 37 | "name": "implementation", 38 | "type": "address" 39 | } 40 | ], 41 | "name": "ERC1967InvalidImplementation", 42 | "type": "error" 43 | }, 44 | { 45 | "inputs": [], 46 | "name": "ERC1967NonPayable", 47 | "type": "error" 48 | }, 49 | { 50 | "inputs": [], 51 | "name": "FailedCall", 52 | "type": "error" 53 | }, 54 | { 55 | "anonymous": false, 56 | "inputs": [ 57 | { 58 | "indexed": true, 59 | "internalType": "address", 60 | "name": "implementation", 61 | "type": "address" 62 | } 63 | ], 64 | "name": "Upgraded", 65 | "type": "event" 66 | }, 67 | { 68 | "stateMutability": "payable", 69 | "type": "fallback" 70 | } 71 | ], 72 | "bytecode": "0x608060405260405161040938038061040983398101604081905261002291610267565b61002c8282610033565b5050610351565b61003c82610092565b6040516001600160a01b038316907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b90600090a280511561008657610081828261010e565b505050565b61008e610185565b5050565b806001600160a01b03163b6000036100cd57604051634c9c8ce360e01b81526001600160a01b03821660048201526024015b60405180910390fd5b7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc80546001600160a01b0319166001600160a01b0392909216919091179055565b6060600080846001600160a01b03168460405161012b9190610335565b600060405180830381855af49150503d8060008114610166576040519150601f19603f3d011682016040523d82523d6000602084013e61016b565b606091505b50909250905061017c8583836101a6565b95945050505050565b34156101a45760405163b398979f60e01b815260040160405180910390fd5b565b6060826101bb576101b682610205565b6101fe565b81511580156101d257506001600160a01b0384163b155b156101fb57604051639996b31560e01b81526001600160a01b03851660048201526024016100c4565b50805b9392505050565b80511561021457805160208201fd5b60405163d6bda27560e01b815260040160405180910390fd5b634e487b7160e01b600052604160045260246000fd5b60005b8381101561025e578181015183820152602001610246565b50506000910152565b6000806040838503121561027a57600080fd5b82516001600160a01b038116811461029157600080fd5b60208401519092506001600160401b03808211156102ae57600080fd5b818501915085601f8301126102c257600080fd5b8151818111156102d4576102d461022d565b604051601f8201601f19908116603f011681019083821181831017156102fc576102fc61022d565b8160405282815288602084870101111561031557600080fd5b610326836020830160208801610243565b80955050505050509250929050565b60008251610347818460208701610243565b9190910192915050565b60aa8061035f6000396000f3fe6080604052600a600c565b005b60186014601a565b6051565b565b6000604c7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc546001600160a01b031690565b905090565b3660008037600080366000845af43d6000803e808015606f573d6000f35b3d6000fdfea264697066735822122060aeada43c964f1fbb27c2d9cbef9ca57ecc24f36b6aed0059d5371ade8a972f64736f6c63430008160033", 73 | "deployedBytecode": "0x6080604052600a600c565b005b60186014601a565b6051565b565b6000604c7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc546001600160a01b031690565b905090565b3660008037600080366000845af43d6000803e808015606f573d6000f35b3d6000fdfea264697066735822122060aeada43c964f1fbb27c2d9cbef9ca57ecc24f36b6aed0059d5371ade8a972f64736f6c63430008160033", 74 | "linkReferences": {}, 75 | "deployedLinkReferences": {} 76 | } -------------------------------------------------------------------------------- /packages/contracts/ignition/deployments/chain-84532/artifacts/AgentsRegistryUpgradeableModule#AgentsRegistryProxy.json: -------------------------------------------------------------------------------- 1 | { 2 | "_format": "hh-sol-artifact-1", 3 | "contractName": "ERC1967Proxy", 4 | "sourceName": "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol", 5 | "abi": [ 6 | { 7 | "inputs": [ 8 | { 9 | "internalType": "address", 10 | "name": "implementation", 11 | "type": "address" 12 | }, 13 | { 14 | "internalType": "bytes", 15 | "name": "_data", 16 | "type": "bytes" 17 | } 18 | ], 19 | "stateMutability": "payable", 20 | "type": "constructor" 21 | }, 22 | { 23 | "inputs": [ 24 | { 25 | "internalType": "address", 26 | "name": "target", 27 | "type": "address" 28 | } 29 | ], 30 | "name": "AddressEmptyCode", 31 | "type": "error" 32 | }, 33 | { 34 | "inputs": [ 35 | { 36 | "internalType": "address", 37 | "name": "implementation", 38 | "type": "address" 39 | } 40 | ], 41 | "name": "ERC1967InvalidImplementation", 42 | "type": "error" 43 | }, 44 | { 45 | "inputs": [], 46 | "name": "ERC1967NonPayable", 47 | "type": "error" 48 | }, 49 | { 50 | "inputs": [], 51 | "name": "FailedCall", 52 | "type": "error" 53 | }, 54 | { 55 | "anonymous": false, 56 | "inputs": [ 57 | { 58 | "indexed": true, 59 | "internalType": "address", 60 | "name": "implementation", 61 | "type": "address" 62 | } 63 | ], 64 | "name": "Upgraded", 65 | "type": "event" 66 | }, 67 | { 68 | "stateMutability": "payable", 69 | "type": "fallback" 70 | } 71 | ], 72 | "bytecode": "0x608060405260405161040938038061040983398101604081905261002291610267565b61002c8282610033565b5050610351565b61003c82610092565b6040516001600160a01b038316907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b90600090a280511561008657610081828261010e565b505050565b61008e610185565b5050565b806001600160a01b03163b6000036100cd57604051634c9c8ce360e01b81526001600160a01b03821660048201526024015b60405180910390fd5b7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc80546001600160a01b0319166001600160a01b0392909216919091179055565b6060600080846001600160a01b03168460405161012b9190610335565b600060405180830381855af49150503d8060008114610166576040519150601f19603f3d011682016040523d82523d6000602084013e61016b565b606091505b50909250905061017c8583836101a6565b95945050505050565b34156101a45760405163b398979f60e01b815260040160405180910390fd5b565b6060826101bb576101b682610205565b6101fe565b81511580156101d257506001600160a01b0384163b155b156101fb57604051639996b31560e01b81526001600160a01b03851660048201526024016100c4565b50805b9392505050565b80511561021457805160208201fd5b60405163d6bda27560e01b815260040160405180910390fd5b634e487b7160e01b600052604160045260246000fd5b60005b8381101561025e578181015183820152602001610246565b50506000910152565b6000806040838503121561027a57600080fd5b82516001600160a01b038116811461029157600080fd5b60208401519092506001600160401b03808211156102ae57600080fd5b818501915085601f8301126102c257600080fd5b8151818111156102d4576102d461022d565b604051601f8201601f19908116603f011681019083821181831017156102fc576102fc61022d565b8160405282815288602084870101111561031557600080fd5b610326836020830160208801610243565b80955050505050509250929050565b60008251610347818460208701610243565b9190910192915050565b60aa8061035f6000396000f3fe6080604052600a600c565b005b60186014601a565b6051565b565b6000604c7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc546001600160a01b031690565b905090565b3660008037600080366000845af43d6000803e808015606f573d6000f35b3d6000fdfea264697066735822122060aeada43c964f1fbb27c2d9cbef9ca57ecc24f36b6aed0059d5371ade8a972f64736f6c63430008160033", 73 | "deployedBytecode": "0x6080604052600a600c565b005b60186014601a565b6051565b565b6000604c7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc546001600160a01b031690565b905090565b3660008037600080366000845af43d6000803e808015606f573d6000f35b3d6000fdfea264697066735822122060aeada43c964f1fbb27c2d9cbef9ca57ecc24f36b6aed0059d5371ade8a972f64736f6c63430008160033", 74 | "linkReferences": {}, 75 | "deployedLinkReferences": {} 76 | } -------------------------------------------------------------------------------- /packages/contracts/ignition/deployments/chain-84532/artifacts/ServiceRegistryUpgradeableModule#ServiceRegistryProxy.json: -------------------------------------------------------------------------------- 1 | { 2 | "_format": "hh-sol-artifact-1", 3 | "contractName": "ERC1967Proxy", 4 | "sourceName": "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol", 5 | "abi": [ 6 | { 7 | "inputs": [ 8 | { 9 | "internalType": "address", 10 | "name": "implementation", 11 | "type": "address" 12 | }, 13 | { 14 | "internalType": "bytes", 15 | "name": "_data", 16 | "type": "bytes" 17 | } 18 | ], 19 | "stateMutability": "payable", 20 | "type": "constructor" 21 | }, 22 | { 23 | "inputs": [ 24 | { 25 | "internalType": "address", 26 | "name": "target", 27 | "type": "address" 28 | } 29 | ], 30 | "name": "AddressEmptyCode", 31 | "type": "error" 32 | }, 33 | { 34 | "inputs": [ 35 | { 36 | "internalType": "address", 37 | "name": "implementation", 38 | "type": "address" 39 | } 40 | ], 41 | "name": "ERC1967InvalidImplementation", 42 | "type": "error" 43 | }, 44 | { 45 | "inputs": [], 46 | "name": "ERC1967NonPayable", 47 | "type": "error" 48 | }, 49 | { 50 | "inputs": [], 51 | "name": "FailedCall", 52 | "type": "error" 53 | }, 54 | { 55 | "anonymous": false, 56 | "inputs": [ 57 | { 58 | "indexed": true, 59 | "internalType": "address", 60 | "name": "implementation", 61 | "type": "address" 62 | } 63 | ], 64 | "name": "Upgraded", 65 | "type": "event" 66 | }, 67 | { 68 | "stateMutability": "payable", 69 | "type": "fallback" 70 | } 71 | ], 72 | "bytecode": "0x608060405260405161040938038061040983398101604081905261002291610267565b61002c8282610033565b5050610351565b61003c82610092565b6040516001600160a01b038316907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b90600090a280511561008657610081828261010e565b505050565b61008e610185565b5050565b806001600160a01b03163b6000036100cd57604051634c9c8ce360e01b81526001600160a01b03821660048201526024015b60405180910390fd5b7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc80546001600160a01b0319166001600160a01b0392909216919091179055565b6060600080846001600160a01b03168460405161012b9190610335565b600060405180830381855af49150503d8060008114610166576040519150601f19603f3d011682016040523d82523d6000602084013e61016b565b606091505b50909250905061017c8583836101a6565b95945050505050565b34156101a45760405163b398979f60e01b815260040160405180910390fd5b565b6060826101bb576101b682610205565b6101fe565b81511580156101d257506001600160a01b0384163b155b156101fb57604051639996b31560e01b81526001600160a01b03851660048201526024016100c4565b50805b9392505050565b80511561021457805160208201fd5b60405163d6bda27560e01b815260040160405180910390fd5b634e487b7160e01b600052604160045260246000fd5b60005b8381101561025e578181015183820152602001610246565b50506000910152565b6000806040838503121561027a57600080fd5b82516001600160a01b038116811461029157600080fd5b60208401519092506001600160401b03808211156102ae57600080fd5b818501915085601f8301126102c257600080fd5b8151818111156102d4576102d461022d565b604051601f8201601f19908116603f011681019083821181831017156102fc576102fc61022d565b8160405282815288602084870101111561031557600080fd5b610326836020830160208801610243565b80955050505050509250929050565b60008251610347818460208701610243565b9190910192915050565b60aa8061035f6000396000f3fe6080604052600a600c565b005b60186014601a565b6051565b565b6000604c7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc546001600160a01b031690565b905090565b3660008037600080366000845af43d6000803e808015606f573d6000f35b3d6000fdfea264697066735822122060aeada43c964f1fbb27c2d9cbef9ca57ecc24f36b6aed0059d5371ade8a972f64736f6c63430008160033", 73 | "deployedBytecode": "0x6080604052600a600c565b005b60186014601a565b6051565b565b6000604c7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc546001600160a01b031690565b905090565b3660008037600080366000845af43d6000803e808015606f573d6000f35b3d6000fdfea264697066735822122060aeada43c964f1fbb27c2d9cbef9ca57ecc24f36b6aed0059d5371ade8a972f64736f6c63430008160033", 74 | "linkReferences": {}, 75 | "deployedLinkReferences": {} 76 | } --------------------------------------------------------------------------------