diff --git a/README.md b/README.md index 0d560c8..e69018c 100644 --- a/README.md +++ b/README.md @@ -152,7 +152,6 @@ await acpClient.init(); const relevantAgents = await acpClient.browseAgents( "", { - cluster: "", // usually not needed sort_by: [AcpAgentSort.SUCCESSFUL_JOB_COUNT], top_k: 5, graduationStatus: AcpGraduationStatus.ALL, @@ -164,7 +163,7 @@ const relevantAgents = await acpClient.browseAgents( const relevantAgents = await acpClient.browseAgents( "", { - cluster: "", // usually not needed + sort_by: [AcpAgentSort.SUCCESSFUL_JOB_COUNT], top_k: 5, graduationStatus: AcpGraduationStatus.ALL, onlineStatus: AcpOnlineStatus.ALL diff --git a/examples/acp-base/external-evaluation-v2/.env.example b/examples/acp-base/external-evaluation-v2/.env.example new file mode 100644 index 0000000..e3e7306 --- /dev/null +++ b/examples/acp-base/external-evaluation-v2/.env.example @@ -0,0 +1,9 @@ +WHITELISTED_WALLET_PRIVATE_KEY=<0x-whitelisted-wallet-private-key> +BUYER_AGENT_WALLET_ADDRESS= +BUYER_ENTITY_ID= + +SELLER_ENTITY_ID= +SELLER_AGENT_WALLET_ADDRESS= + +EVALUATOR_AGENT_WALLET_ADDRESS= +EVALUATOR_ENTITY_ID= diff --git a/examples/acp-base/external-evaluation-v2/README.md b/examples/acp-base/external-evaluation-v2/README.md new file mode 100644 index 0000000..b976790 --- /dev/null +++ b/examples/acp-base/external-evaluation-v2/README.md @@ -0,0 +1,84 @@ +# ACP External Evaluation v2 Example + +This example demonstrates **ACP v2** integration flows using a buyer-seller interaction pattern with external evaluation. + +## Overview + +This example showcases use cases enabled by ACP v2's job and payment framework with external evaluation: +- **External Evaluation**: Third-party evaluator validates job completion +- **Job Management**: Complete job lifecycle with evaluation by external agent +- **Agent Discovery**: Finding and selecting service providers +- **Multi-Agent Architecture**: Buyer, seller, and evaluator agents working together + +## Files + +### `buyer.ts` - Service Requester +The buyer agent demonstrates how to: +- **Initiate Jobs**: Find service providers and create jobs +- **Specify Evaluator**: Use external evaluator instead of self-evaluation +- **Handle Payments**: Automatic payment processing during negotiation +- **Job Monitoring**: Track job status through phases + +### `seller.ts` - Service Provider +The seller agent demonstrates how to: +- **Accept Requests**: Handle incoming job requests +- **Provide Services**: Execute requested services +- **Deliver Results**: Submit deliverables for evaluation +- **Job Lifecycle**: Handle REQUEST and TRANSACTION phases + +### `evaluator.ts` - External Evaluator +The evaluator agent demonstrates how to: +- **External Evaluation**: Independent job completion assessment +- **Queue Processing**: Handle multiple evaluation requests +- **Evaluation Logic**: Validate and approve/reject job deliverables +- **Separation of Concerns**: Independent evaluation process + +## Setup + +1. **Environment Configuration**: + ```bash + cp .env.example .env + # Update .env with your credentials + ``` + +2. **Required Environment Variables**: + - `BUYER_AGENT_WALLET_ADDRESS`: Smart wallet address for buyer agent + - `SELLER_AGENT_WALLET_ADDRESS`: Smart wallet address for seller agent + - `EVALUATOR_AGENT_WALLET_ADDRESS`: Smart wallet address for evaluator agent + - `BUYER_ENTITY_ID`: Session entity ID for buyer + - `SELLER_ENTITY_ID`: Session entity ID for seller + - `EVALUATOR_ENTITY_ID`: Session entity ID for evaluator + - `WHITELISTED_WALLET_PRIVATE_KEY`: Private key for whitelisted wallet + +3. **Install Dependencies**: + ```bash + npm install + ``` + +## Running the Example + +### Start the Evaluator (External Evaluator) +```bash +cd examples/acp-base/external-evaluation-v2 +npx ts-node evaluator.ts +``` + +### Start the Seller (Service Provider) +```bash +cd examples/acp-base/external-evaluation-v2 +npx ts-node seller.ts +``` + +### Start the Buyer (Client) +```bash +cd examples/acp-base/external-evaluation-v2 +npx ts-node buyer.ts +``` + +## Usage Flow + +1. **Job Initiation**: Buyer searches for service providers and initiates a job with external evaluator specified +2. **Service Provision**: Seller accepts the job request and provides the requested service +3. **Delivery**: Seller delivers the completed work/results +4. **External Evaluation**: External evaluator (not the buyer) validates the deliverable +5. **Job Completion**: Job is marked as completed based on external evaluation diff --git a/examples/acp-base/external-evaluation-v2/buyer.ts b/examples/acp-base/external-evaluation-v2/buyer.ts new file mode 100644 index 0000000..452bb41 --- /dev/null +++ b/examples/acp-base/external-evaluation-v2/buyer.ts @@ -0,0 +1,72 @@ +import AcpClient, { + AcpContractClientV2, + AcpJobPhases, + AcpJob, + AcpMemo, + AcpAgentSort, + AcpGraduationStatus, + AcpOnlineStatus +} from "../../../src"; +import { + BUYER_AGENT_WALLET_ADDRESS, + WHITELISTED_WALLET_PRIVATE_KEY, + BUYER_ENTITY_ID, + EVALUATOR_AGENT_WALLET_ADDRESS +} from "./env"; + +async function buyer() { + const acpClient = new AcpClient({ + acpContractClient: await AcpContractClientV2.build( + WHITELISTED_WALLET_PRIVATE_KEY, + BUYER_ENTITY_ID, + BUYER_AGENT_WALLET_ADDRESS + ), + onNewTask: async (job: AcpJob, memoToSign?: AcpMemo) => { + if ( + job.phase === AcpJobPhases.NEGOTIATION && + memoToSign?.nextPhase === AcpJobPhases.TRANSACTION + ) { + console.log(`Paying for job ${job.id}`); + await job.payAndAcceptRequirement(); + console.log(`Job ${job.id} paid`); + } else if ( + job.phase === AcpJobPhases.TRANSACTION && + memoToSign?.nextPhase === AcpJobPhases.REJECTED + ) { + console.log("Signing job rejection memo", job); + console.log(`Signing job ${job.id} rejection memo, rejection reason: ${memoToSign?.content}`); + console.log(`Job ${job.id} rejection memo signed`); + } else if (job.phase === AcpJobPhases.COMPLETED) { + console.log(`Job ${job.id} completed, received deliverable:`, job.deliverable); + } else if (job.phase === AcpJobPhases.REJECTED) { + console.log(`Job ${job.id} rejected`); + } + } + }); + + // Browse available agents based on a keyword + const relevantAgents = await acpClient.browseAgents( + "", + { + sort_by: [AcpAgentSort.SUCCESSFUL_JOB_COUNT], + top_k: 5, + graduationStatus: AcpGraduationStatus.ALL, + onlineStatus: AcpOnlineStatus.ALL, + } + ); + + // Pick one of the agents based on your criteria (in this example we just pick the first one) + const chosenAgent = relevantAgents[0]; + // Pick one of the service offerings based on your criteria (in this example we just pick the first one) + const chosenJobOffering = chosenAgent.jobOfferings[0]; // v2 uses jobOfferings instead of offerings + + const jobId = await chosenJobOffering.initiateJob( + "Help me to generate a flower meme.", + EVALUATOR_AGENT_WALLET_ADDRESS, // Use external evaluator address + new Date(Date.now() + 1000 * 60 * 60 * 24) // expiredAt + ); + + console.log(`Job ${jobId} initiated`); +} + +buyer(); diff --git a/examples/acp-base/external-evaluation-v2/env.ts b/examples/acp-base/external-evaluation-v2/env.ts new file mode 100644 index 0000000..7083df6 --- /dev/null +++ b/examples/acp-base/external-evaluation-v2/env.ts @@ -0,0 +1,44 @@ +import dotenv from "dotenv"; +import { Address } from "viem"; + +dotenv.config({ path: __dirname + "/.env" }); + +function getEnvVar(key: string, required = true): T { + const value = process.env[key]; + if (required && (value === undefined || value === "")) { + throw new Error(`${key} is not defined or is empty in the .env file`); + } + return value as T; +} + +export const WHITELISTED_WALLET_PRIVATE_KEY = getEnvVar
( + "WHITELISTED_WALLET_PRIVATE_KEY" +); + +export const BUYER_AGENT_WALLET_ADDRESS = getEnvVar
( + "BUYER_AGENT_WALLET_ADDRESS" +); + +export const BUYER_ENTITY_ID = parseInt(getEnvVar("BUYER_ENTITY_ID")); + +export const SELLER_AGENT_WALLET_ADDRESS = getEnvVar
( + "SELLER_AGENT_WALLET_ADDRESS" +); + +export const SELLER_ENTITY_ID = parseInt(getEnvVar("SELLER_ENTITY_ID")); + +export const EVALUATOR_AGENT_WALLET_ADDRESS = getEnvVar
( + "EVALUATOR_AGENT_WALLET_ADDRESS" +); + +export const EVALUATOR_ENTITY_ID = parseInt(getEnvVar("EVALUATOR_ENTITY_ID")); + +const entities = { + BUYER_ENTITY_ID, + SELLER_ENTITY_ID, + EVALUATOR_ENTITY_ID, +}; + +for (const [key, value] of Object.entries(entities)) { + if (isNaN(value)) throw new Error(`${key} must be a valid number`); +} diff --git a/examples/acp-base/external-evaluation-v2/evaluator.ts b/examples/acp-base/external-evaluation-v2/evaluator.ts new file mode 100644 index 0000000..79b196a --- /dev/null +++ b/examples/acp-base/external-evaluation-v2/evaluator.ts @@ -0,0 +1,32 @@ +import AcpClient, { + AcpContractClientV2, + AcpJob +} from '../../../src'; +import { + EVALUATOR_AGENT_WALLET_ADDRESS, + EVALUATOR_ENTITY_ID, + WHITELISTED_WALLET_PRIVATE_KEY +} from "./env"; + +async function evaluator() { + new AcpClient({ + acpContractClient: await AcpContractClientV2.build( + WHITELISTED_WALLET_PRIVATE_KEY, + EVALUATOR_ENTITY_ID, + EVALUATOR_AGENT_WALLET_ADDRESS + ), + onEvaluate: async (job: AcpJob) => { + console.log("[onEvaluate] Evaluation function called", job.memos); + try { + await job.evaluate(true, "Externally evaluated and approved"); + console.log(`[onEvaluate] Job ${job.id} evaluated`); + } catch (err) { + console.error(`[onEvaluate] Job ${job.id}:`, err); + } + } + }); + + console.log("[Evaluator] Listening for new jobs..."); +} + +evaluator(); diff --git a/examples/acp-base/external-evaluation-v2/seller.ts b/examples/acp-base/external-evaluation-v2/seller.ts new file mode 100644 index 0000000..5117367 --- /dev/null +++ b/examples/acp-base/external-evaluation-v2/seller.ts @@ -0,0 +1,57 @@ +import AcpClient, { + AcpContractClientV2, + AcpJobPhases, + AcpJob, + AcpMemo, + IDeliverable +} from '../../../src'; +import { + SELLER_AGENT_WALLET_ADDRESS, + SELLER_ENTITY_ID, + WHITELISTED_WALLET_PRIVATE_KEY +} from "./env"; + +const REJECT_JOB = false + +async function seller() { + new AcpClient({ + acpContractClient: await AcpContractClientV2.build( + WHITELISTED_WALLET_PRIVATE_KEY, + SELLER_ENTITY_ID, + SELLER_AGENT_WALLET_ADDRESS + ), + onNewTask: async (job: AcpJob, memoToSign?: AcpMemo) => { + if ( + job.phase === AcpJobPhases.REQUEST && + job.memos.find((m) => m.nextPhase === AcpJobPhases.NEGOTIATION) + ) { + const response = true; + console.log(`Responding to job ${job.id} with requirement`, job.requirement); + await job.respond(response); + console.log(`Job ${job.id} responded with ${response}`); + } else if ( + job.phase === AcpJobPhases.TRANSACTION && + memoToSign?.nextPhase === AcpJobPhases.EVALUATION + ) { + // to cater cases where agent decide to reject job after payment has been made + if (REJECT_JOB) { // conditional check for job rejection logic + const reason = "Job requirement does not meet agent capability"; + console.log(`Rejecting job ${job.id} with reason: ${reason}`) + await job.respond(false, reason); + console.log(`Job ${job.id} rejected`); + return; + } + + const deliverable: IDeliverable = { + type: "url", + value: "https://example.com", + } + console.log(`Delivering job ${job.id} with deliverable`, deliverable); + await job.deliver(deliverable); + console.log(`Job ${job.id} delivered`); + } + } + }); +} + +seller(); diff --git a/examples/acp-base/external-evaluation/README.md b/examples/acp-base/external-evaluation/README.md index 8622a87..7456c58 100644 --- a/examples/acp-base/external-evaluation/README.md +++ b/examples/acp-base/external-evaluation/README.md @@ -116,7 +116,6 @@ You can customize agent discovery and job selection using: const relevantAgents = await acpClient.browseAgents( "", { - cluster: "", sort_by: [""], rerank: "", top_k: "", diff --git a/examples/acp-base/external-evaluation/buyer.ts b/examples/acp-base/external-evaluation/buyer.ts index 650375c..b72bcb9 100644 --- a/examples/acp-base/external-evaluation/buyer.ts +++ b/examples/acp-base/external-evaluation/buyer.ts @@ -38,9 +38,8 @@ async function buyer() { // Browse available agents based on a keyword and cluster name const relevantAgents = await acpClient.browseAgents( - "alpha generating agnt", + "", { - cluster: "", sort_by: [AcpAgentSort.SUCCESSFUL_JOB_COUNT], top_k: 5, graduationStatus: AcpGraduationStatus.ALL, diff --git a/examples/acp-base/funds-v2/README.md b/examples/acp-base/funds-v2/README.md new file mode 100644 index 0000000..6052895 --- /dev/null +++ b/examples/acp-base/funds-v2/README.md @@ -0,0 +1,126 @@ +# ACP v2 Example + +This example demonstrates **ACP v2** integration flows using a buyer-seller interaction pattern. + +## Overview + +This example showcases use cases enabled by ACP v2's job and payment framework: +- **Position Management**: Custom job definitions for opening and closing trading positions +- **Token Swapping**: User-defined jobs for swapping between different tokens +- **Fund Transfers**: Utilizing ACP's escrow and transfer infrastructure + +## Files + +### `buyer.ts` - Fund Management Client +The buyer agent demonstrates how to: +- **Initiate Jobs**: Find fund management service providers and create trading jobs +- **Open Positions**: Create trading positions with take-profit and stop-loss parameters +- **Close Positions**: Close existing trading positions +- **Swap Tokens**: Perform token swaps through the service provider +- **Interactive CLI**: Provides a command-line interface for real-time interaction + +**Key Features:** +- Automatic payment handling for negotiation phase +- Interactive menu for testing different fund operations +- Real-time job status monitoring +- Support for multiple position management operations + +### `seller.ts` - Fund Management Service Provider +The seller agent demonstrates how to: +- **Accept Fund Requests**: Handle incoming fund management requests +- **Process Payments**: Manage payable memos and escrow transfers +- **Provide Services**: Execute fund management operations +- **Client State Management**: Track client wallets, assets, and positions +- **Task Handling**: Support multiple task types (open/close positions, swaps, withdrawals) + +**Supported Task Types:** +- `OPEN_POSITION`: Create new trading positions +- `CLOSE_POSITION`: Close existing positions +- `SWAP_TOKEN`: Perform token swaps + +## Setup + +1. **Environment Configuration**: + ```bash + cp .env.example .env + # Update .env with your credentials + ``` + +2. **Required Environment Variables**: + - `BUYER_AGENT_WALLET_ADDRESS`: Smart wallet address for buyer agent + - `SELLER_AGENT_WALLET_ADDRESS`: Smart wallet address for seller agent + - `BUYER_ENTITY_ID`: Session entity ID for buyer + - `SELLER_ENTITY_ID`: Session entity ID for seller + - `WHITELISTED_WALLET_PRIVATE_KEY`: Private key for whitelisted wallet + +3. **Install Dependencies**: + ```bash + npm install + ``` + +## Running the Example + +### Start the Seller (Service Provider) +```bash +cd examples/acp-base/self-evaluation-v2 +npx ts-node seller.ts +``` + +### Start the Buyer (Client) +```bash +cd examples/acp-base/self-evaluation-v2 +npx ts-node buyer.ts +``` + +## Usage Flow + +1. **Job Initiation**: Buyer searches for seller agents and initiates a job +2. **Service Selection**: Buyer can perform various fund management operations: + - Open trading positions with TP/SL parameters + - Close existing positions + - Swap tokens (e.g., USDC to USD) + - Close the entire job + +3. **Interactive Operations**: Use the CLI menu to test different scenarios: + ``` + Available actions: + 1. Open position + 2. Close position + 3. Swap token + ``` + +4. **Payment Handling**: The system automatically handles: + - Escrow payments for position operations + - Transfer confirmations + - Fee management + +## ACP v2 Features + +This example demonstrates use cases enabled by ACP v2: + +- **Enhanced Position Management**: Example of how users can define custom jobs for complex trading positions with risk management +- **Multi-Asset Support**: Shows user-defined job offerings for various token types and trading pairs +- **Escrow Integration**: Uses ACP's basic escrow infrastructure - actual trading logic is user-defined +- **Real-time State Tracking**: Custom implementation of portfolio monitoring using ACP's job messaging +- **Advanced Payment Flows**: Examples of different payment patterns using ACP's payment infrastructure + +Note: All features are user-defined through custom job offerings. + +## Reference Documentation + +For detailed information about ACP v2 integration flows and use cases, see: +[ACP v2 Integration Flows & Use Cases](https://virtualsprotocol.notion.site/ACP-Fund-Transfer-v2-Integration-Flows-Use-Cases-2632d2a429e980c2b263d1129a417a2b) + +## Notes + +- This example uses the Base Sepolia testnet configuration +- The buyer agent automatically pays with 0 amount for testing purposes +- Position parameters (TP/SL percentages, amounts) are configurable +- All fund operations are simulated for demonstration purposes + +## Troubleshooting + +- Ensure both agents are registered and whitelisted on the ACP platform +- Verify environment variables are correctly set +- Check that the seller agent is running before starting the buyer +- Monitor console output for job status updates and error messages diff --git a/examples/acp-base/funds-v2/trading/.env.example b/examples/acp-base/funds-v2/trading/.env.example new file mode 100644 index 0000000..80ca9b7 --- /dev/null +++ b/examples/acp-base/funds-v2/trading/.env.example @@ -0,0 +1,6 @@ +WHITELISTED_WALLET_PRIVATE_KEY=<0x-whitelisted-wallet-private-key> +BUYER_AGENT_WALLET_ADDRESS= +BUYER_ENTITY_ID= + +SELLER_ENTITY_ID= +SELLER_AGENT_WALLET_ADDRESS= diff --git a/examples/acp-base/funds-v2/trading/buyer.ts b/examples/acp-base/funds-v2/trading/buyer.ts new file mode 100644 index 0000000..d410a2d --- /dev/null +++ b/examples/acp-base/funds-v2/trading/buyer.ts @@ -0,0 +1,161 @@ +import * as readline from "readline"; +import AcpClient, { + AcpAgentSort, + AcpContractClientV2, + AcpGraduationStatus, + AcpJob, + AcpJobPhases, + AcpMemo, + AcpOnlineStatus, + baseAcpConfigV2, + FareAmount, + MemoType, +} from "../../../../src"; +import { + BUYER_AGENT_WALLET_ADDRESS, + BUYER_ENTITY_ID, + WHITELISTED_WALLET_PRIVATE_KEY, +} from "./env"; +import { FundsV2DemoJobPayload } from "./jobTypes"; + +async function sleep(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, +}); + +const question = (prompt: string): Promise => { + return new Promise((resolve) => { + rl.question(prompt, (answer) => { + resolve(answer.trim()); + }); + }); +}; + +const SERVICE_REQUIREMENTS_JOB_TYPE_MAPPING: Record = { + swap_token: { + fromSymbol: "USDC", + fromContractAddress: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // Base USDC Token + amount: 0.008, + toSymbol: "VIRTUAL", + toContractAddress: "0x0b3e328455c4059EEb9e3f84b5543F74E24e7E1b" + }, + open_position: { + symbol: "BTC", + amount: 0.009, + tp: { percentage: 5 }, + sl: { percentage: 2 }, + direction: "long", + }, + close_position: { symbol: "BTC" }, +} + +async function main() { + let currentJobId: number | null = null; + + const acpClient = new AcpClient({ + acpContractClient: await AcpContractClientV2.build( + WHITELISTED_WALLET_PRIVATE_KEY, + BUYER_ENTITY_ID, + BUYER_AGENT_WALLET_ADDRESS + ), + onNewTask: async (job: AcpJob, memoToSign?: AcpMemo) => { + const { id: jobId, phase: jobPhase } = job; + if (!memoToSign) { + if (job.phase === AcpJobPhases.REJECTED || job.phase === AcpJobPhases.COMPLETED) { + currentJobId = null; + console.log(`[onNewTask] Job ${jobId} ${AcpJobPhases[jobPhase]}`); + return; + } + console.log("[onNewTask] No memo to sign", { jobId }); + return; + } + const memoId = memoToSign.id; + console.log("[onNewTask] New job received", { jobId, memoId, phase: AcpJobPhases[jobPhase] }); + + if ( + jobPhase === AcpJobPhases.NEGOTIATION && + memoToSign.nextPhase === AcpJobPhases.TRANSACTION + ) { + console.log(`[onNewTask] Paying for job ${jobId}`); + await job.payAndAcceptRequirement(); + currentJobId = jobId; + console.log(`[onNewTask] Job ${jobId} paid`); + } else if ( + jobPhase === AcpJobPhases.TRANSACTION + ) { + if (memoToSign.nextPhase === AcpJobPhases.REJECTED) { + console.log("[onNewTask] Signing job rejection memo", { jobId, memoId }); + await memoToSign.sign(true, "Accepts job rejection"); + console.log("[onNewTask] Rejection memo signed", { jobId }); + currentJobId = null; + } else if ( + memoToSign.nextPhase === AcpJobPhases.TRANSACTION && + memoToSign.type === MemoType.PAYABLE_TRANSFER_ESCROW + ) { + console.log("[onNewTask] Accepting funds transfer", { jobId, memoId }); + await memoToSign.sign(true, "Accepts funds transfer"); + console.log("[onNewTask] Funds transfer memo signed", { jobId }); + } + } else if (memoToSign.type === MemoType.NOTIFICATION || memoToSign.type === MemoType.PAYABLE_NOTIFICATION) { + console.log(`[onNewTask] Job ${jobId} received notification: ${memoToSign.content}`); + await memoToSign.sign(true, "Acknowledged on job update notification"); + } + } + }); + + const agents = await acpClient.browseAgents( + "", + { + sort_by: [AcpAgentSort.SUCCESSFUL_JOB_COUNT], + top_k: 5, + graduationStatus: AcpGraduationStatus.ALL, + onlineStatus: AcpOnlineStatus.ALL, + } + ); + console.log(agents); + const { jobOfferings } = agents[0]; + console.log(jobOfferings); + const actionsDefinition = (jobOfferings ?? []) + .map((offering, idx) => { + return { + index: idx + 1, + desc: offering.name, + action: async() => { + currentJobId = await offering.initiateJob(SERVICE_REQUIREMENTS_JOB_TYPE_MAPPING[offering.name]) + }, + }; + }) + + while (true) { + await sleep(100); + if (currentJobId) { + // No job found, waiting for new job + continue; + } + + console.log("\nAvailable actions:"); + actionsDefinition.forEach((action) => { + console.log(`${action.index}. ${action.desc}`); + }); + + const answer = await question("\nSelect an action (enter the number): "); + console.log("Initiating job..."); + const selectedIndex = parseInt(answer, 10); + + const selectedAction = actionsDefinition.find( + (action) => action.index === selectedIndex + ); + + if (selectedAction) { + await selectedAction.action(); + } else { + console.log("Invalid selection. Please try again."); + } + } +} + +main(); diff --git a/examples/acp-base/funds-v2/trading/env.ts b/examples/acp-base/funds-v2/trading/env.ts new file mode 100644 index 0000000..612821b --- /dev/null +++ b/examples/acp-base/funds-v2/trading/env.ts @@ -0,0 +1,37 @@ +import dotenv from "dotenv"; +import { Address } from "viem"; + +dotenv.config({ path: __dirname + "/.env" }); + +function getEnvVar(key: string, required = true): T { + const value = process.env[key]; + if (required && (value === undefined || value === "")) { + throw new Error(`${key} is not defined or is empty in the .env file`); + } + return value as T; +} + +export const WHITELISTED_WALLET_PRIVATE_KEY = getEnvVar
( + "WHITELISTED_WALLET_PRIVATE_KEY" +); + +export const BUYER_AGENT_WALLET_ADDRESS = getEnvVar
( + "BUYER_AGENT_WALLET_ADDRESS" +); + +export const BUYER_ENTITY_ID = parseInt(getEnvVar("BUYER_ENTITY_ID")); + +export const SELLER_AGENT_WALLET_ADDRESS = getEnvVar
( + "SELLER_AGENT_WALLET_ADDRESS" +); + +export const SELLER_ENTITY_ID = parseInt(getEnvVar("SELLER_ENTITY_ID")); + +const entities = { + BUYER_ENTITY_ID, + SELLER_ENTITY_ID, +}; + +for (const [key, value] of Object.entries(entities)) { + if (isNaN(value)) throw new Error(`${key} must be a valid number`); +} diff --git a/examples/acp-base/funds-v2/trading/jobTypes.ts b/examples/acp-base/funds-v2/trading/jobTypes.ts new file mode 100644 index 0000000..1a856e3 --- /dev/null +++ b/examples/acp-base/funds-v2/trading/jobTypes.ts @@ -0,0 +1,29 @@ +export type V2DemoSwapTokenPayload = { + fromSymbol: string; + fromContractAddress: `0x${string}`; + amount: number; + toSymbol: string; + toContractAddress: `0x${string}`; +} + +export type TpSlConfig = { + percentage?: number; + price?: number; +} + +export type V2DemoOpenPositionPayload = { + symbol: string; + amount: number; + tp: TpSlConfig; + sl: TpSlConfig; + direction: "long" | "short" +} + +export type V2DemoClosePositionPayload = { + symbol: string +} + +export type FundsV2DemoJobPayload = + | V2DemoSwapTokenPayload + | V2DemoOpenPositionPayload + | V2DemoClosePositionPayload; diff --git a/examples/acp-base/funds-v2/trading/seller.ts b/examples/acp-base/funds-v2/trading/seller.ts new file mode 100644 index 0000000..7dd0aa8 --- /dev/null +++ b/examples/acp-base/funds-v2/trading/seller.ts @@ -0,0 +1,301 @@ +import dotenv from "dotenv"; +import AcpClient, { + AcpContractClientV2, + AcpJob, + AcpJobPhases, + AcpMemo, + baseAcpConfigV2, + Fare, + FareAmount, + MemoType, +} from "../../../../src"; +import { Address } from "viem"; +import { createHash } from "crypto"; +import { + SELLER_AGENT_WALLET_ADDRESS, + SELLER_ENTITY_ID, + WHITELISTED_WALLET_PRIVATE_KEY +} from "./env"; +import { + TpSlConfig, + V2DemoClosePositionPayload, + V2DemoOpenPositionPayload, + V2DemoSwapTokenPayload +} from "./jobTypes"; +import readline from "readline"; + +dotenv.config(); + +const config = baseAcpConfigV2; + +enum JobName { + OPEN_POSITION = "open_position", + CLOSE_POSITION = "close_position", + SWAP_TOKEN = "swap_token", +} + +interface IPosition { + symbol: string; + amount: number; + tp: TpSlConfig; + sl: TpSlConfig; +} + +interface IClientWallet { + clientAddress: Address; + positions: IPosition[]; +} + +const client: Record = {}; + +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, +}); + +const question = (prompt: string): Promise => { + return new Promise((resolve) => { + rl.question(prompt, (answer) => { + resolve(answer.trim()); + }); + }); +}; + +const promptTpSlAction = async (job: AcpJob, wallet: IClientWallet) => { + console.log("\nClient wallet:\n", wallet); + const positions = wallet.positions.filter((p) => p.amount > 0); + if (positions.length) { + console.log("\nAvailable actions:"); + console.log("1. Hit TP"); + console.log("2. Hit SL\n") + const tpSlAnswer = await question("Select an action (enter the number): "); + const selectedIndex = parseInt(tpSlAnswer, 10); + let selectedAction: string | null = null; + if (selectedIndex === 1) { + selectedAction = "TP"; + } else if (selectedIndex === 2) { + selectedAction = "SL"; + } + + if (selectedAction) { + let validTokenSymbol: boolean = false; + let position: IPosition | undefined; + while (!validTokenSymbol) { + const tokenSymbolAnswer = await question("Token symbol to close: "); + position = wallet.positions.find((p) => p.symbol.toLowerCase() === tokenSymbolAnswer.toLowerCase()); + validTokenSymbol = !!position && position.amount > 0 + } + if (position) { + console.log(`${position.symbol} position hits ${selectedAction}, sending remaining funds back to buyer`); + closePosition(wallet, position.symbol); + await job.createPayableNotification( + `${position.symbol} position has hit ${selectedAction}. Closed ${position.symbol} position with txn hash 0x0f60a30d66f1f3d21bad63e4e53e59d94ae286104fe8ea98f28425821edbca1b`, + new FareAmount( + position.amount * ( + selectedAction === "TP" + ? 1 + ((position.tp?.percentage || 0) / 100) + : 1 - ((position.sl?.percentage || 0) / 100) + ), + config.baseFare + ), + ); + console.log(`${position.symbol} position funds sent back to buyer`); + console.log(wallet); + } + } else { + console.log("Invalid selection. Please try again."); + } + } +} + +const getClientWallet = (address: Address): IClientWallet => { + const hash = createHash("sha256").update(address).digest("hex"); + const walletAddress = `0x${hash}` as Address; + + if (!client[walletAddress]) { + client[walletAddress] = { + clientAddress: walletAddress, + positions: [], + }; + } + + return client[walletAddress]; +}; + +const onNewTask = async (job: AcpJob, memoToSign?: AcpMemo) => { + const { id: jobId, phase: jobPhase, name: jobName } = job; + if (!memoToSign) { + console.log("[onNewTask] No memo to sign", { jobId }); + return; + } + const memoId = memoToSign.id; + + console.info("[onNewTask] Received job", { jobId, phase: AcpJobPhases[jobPhase], jobName, memoId }); + + if (jobPhase === AcpJobPhases.REQUEST) { + return await handleTaskRequest(job, memoToSign); + } else if (jobPhase === AcpJobPhases.TRANSACTION) { + return await handleTaskTransaction(job); + } +}; + +const handleTaskRequest = async (job: AcpJob, memoToSign?: AcpMemo) => { + const { id: jobId, name: jobName } = job; + const memoId = memoToSign?.id; + + if (!memoToSign || !jobName) { + console.error("[handleTaskRequest] Missing data", { jobId, memoId, jobName }); + return; + } + + switch (jobName) { + case JobName.OPEN_POSITION: { + console.log("Accepts position opening request", job.requirement); + await memoToSign.sign(true, "Accepts position opening"); + const openPositionPayload = job.requirement as V2DemoOpenPositionPayload; + return await job.createPayableRequirement( + "Send me USDC to open position", + MemoType.PAYABLE_REQUEST, + new FareAmount( + openPositionPayload.amount, + config.baseFare // Open position against ACP Base Currency: USDC + ), + job.providerAddress + ); + } + + case JobName.CLOSE_POSITION: { + const wallet = getClientWallet(job.clientAddress); + const closePositionPayload = job.requirement as V2DemoClosePositionPayload; + + const symbol = closePositionPayload.symbol; + const position = wallet.positions.find((p) => p.symbol === symbol); + const positionIsValid = !!position && position.amount > 0 + const response = positionIsValid + ? `Accepts position closing. Please make payment to close ${symbol} position.` + : "Rejects position closing. Position is invalid."; + console.log(response); + return await job.respond(positionIsValid, response); + } + + case JobName.SWAP_TOKEN: { + console.log("Accepts token swapping request", job.requirement); + await memoToSign.sign(true, "Accepts token swapping request"); + + const swapTokenPayload = job.requirement as V2DemoSwapTokenPayload; + + return await job.createPayableRequirement( + `Send me ${swapTokenPayload.fromSymbol} to swap to ${swapTokenPayload.toSymbol}`, + MemoType.PAYABLE_REQUEST, + new FareAmount( + swapTokenPayload.amount, + await Fare.fromContractAddress( // Constructing Fare for the token to swap from + swapTokenPayload.fromContractAddress, + config + ) + ), + job.providerAddress + ); + } + + default: + console.warn("[handleTaskRequest] Unsupported job name", { jobId, jobName }); + } +}; + +const handleTaskTransaction = async (job: AcpJob) => { + const { id: jobId, name: jobName } = job; + const wallet = getClientWallet(job.clientAddress); + + if (!jobName) { + console.error("[handleTaskTransaction] Missing job name", { jobId }); + return; + } + + switch (jobName) { + case JobName.OPEN_POSITION: { + const openPositionPayload = job.requirement as V2DemoOpenPositionPayload; + openPosition(wallet, openPositionPayload); + console.log("Opening position", openPositionPayload); + await job.deliver({ + type: "message", + value: "Opened position with txn 0x71c038a47fd90069f133e991c4f19093e37bef26ca5c78398b9c99687395a97a" + }); + console.log("Position opened"); + return await promptTpSlAction(job, wallet); + } + + case JobName.CLOSE_POSITION: { + const closePositionPayload = job.requirement as V2DemoClosePositionPayload; + const closingAmount = closePosition(wallet, closePositionPayload.symbol) || 0; + console.log(`Returning closing amount: ${closingAmount} USDC`); + await job.deliverPayable( + { + type: "message", + value: `Closed ${closePositionPayload.symbol} position with txn hash 0x0f60a30d66f1f3d21bad63e4e53e59d94ae286104fe8ea98f28425821edbca1b`, + }, + new FareAmount( + closingAmount, + config.baseFare + ) + ); + console.log("Closing amount returned"); + console.log(wallet); + break; + } + + case JobName.SWAP_TOKEN: { + const swapTokenPayload = job.requirement as V2DemoSwapTokenPayload; + const swappedTokenPayload = { + symbol: swapTokenPayload.toSymbol, + amount: new FareAmount( + 0.00088, + await Fare.fromContractAddress( // Constructing Fare for the token to swap to + swapTokenPayload.toContractAddress, + config + ) + ) + } + console.log("Returning swapped token", swappedTokenPayload); + await job.deliverPayable( + { + type: "message", + value: `Return swapped token ${swappedTokenPayload.symbol}` + }, + swappedTokenPayload.amount + ); + console.log("Swapped token returned"); + break; + } + + default: + console.warn("[handleTaskTransaction] Unsupported job name", { jobId, jobName }); + } +}; + +function openPosition(wallet: IClientWallet, payload: V2DemoOpenPositionPayload) { + const { symbol, amount, tp, sl } = payload; + const pos = wallet.positions.find((p) => p.symbol === symbol); + if (pos) pos.amount += payload.amount; + else wallet.positions.push({ symbol, amount, tp, sl }); +} + +function closePosition(wallet: IClientWallet, symbol: string): number | undefined { + const pos = wallet.positions.find((p) => p.symbol === symbol); + // remove the position from wallet + wallet.positions = wallet.positions.filter((p) => p.symbol !== symbol); + return pos?.amount; +} + +async function main() { + new AcpClient({ + acpContractClient: await AcpContractClientV2.build( + WHITELISTED_WALLET_PRIVATE_KEY, + SELLER_ENTITY_ID, + SELLER_AGENT_WALLET_ADDRESS + ), + onNewTask, + }); +} + +main(); diff --git a/examples/acp-base/funds/README.md b/examples/acp-base/funds/README.md index 517b1f9..361690b 100644 --- a/examples/acp-base/funds/README.md +++ b/examples/acp-base/funds/README.md @@ -196,7 +196,6 @@ const acpClient = new AcpClient({ const relevantAgents = await acpClient.browseAgents( "", { - cluster: "", sort_by: [AcpAgentSort.SUCCESSFUL_JOB_COUNT], top_k: 5, graduationStatus: AcpGraduationStatus.ALL, diff --git a/examples/acp-base/funds/buyer.ts b/examples/acp-base/funds/buyer.ts index f2b9719..a5a7ba1 100644 --- a/examples/acp-base/funds/buyer.ts +++ b/examples/acp-base/funds/buyer.ts @@ -183,7 +183,6 @@ async function buyer() { const relevantAgents = await acpClient.browseAgents( "", { - cluster: "", sort_by: [AcpAgentSort.SUCCESSFUL_JOB_COUNT], top_k: 5, graduationStatus: AcpGraduationStatus.ALL, diff --git a/examples/acp-base/polling-mode/buyer.ts b/examples/acp-base/polling-mode/buyer.ts index 572fa61..7d7d3ea 100644 --- a/examples/acp-base/polling-mode/buyer.ts +++ b/examples/acp-base/polling-mode/buyer.ts @@ -1,8 +1,9 @@ import AcpClient, { - AcpContractClient, + AcpContractClientV2, AcpJobPhases, AcpGraduationStatus, AcpOnlineStatus, + AcpAgentSort } from "@virtuals-protocol/acp-node"; import { BUYER_AGENT_WALLET_ADDRESS, @@ -21,7 +22,7 @@ async function sleep(ms: number) { async function buyer() { const acpClient = new AcpClient({ - acpContractClient: await AcpContractClient.build( + acpContractClient: await AcpContractClientV2.build( WHITELISTED_WALLET_PRIVATE_KEY, BUYER_ENTITY_ID, BUYER_AGENT_WALLET_ADDRESS, @@ -34,9 +35,10 @@ async function buyer() { const relevantAgents = await acpClient.browseAgents( "", { - cluster: "", + sort_by: [AcpAgentSort.SUCCESSFUL_JOB_COUNT], + top_k: 5, graduationStatus: AcpGraduationStatus.ALL, - onlineStatus: AcpOnlineStatus.ALL, + onlineStatus: AcpOnlineStatus.ALL } ); console.log("Relevant agents:", relevantAgents); @@ -80,8 +82,9 @@ async function buyer() { // Check if there's a memo that indicates next phase is TRANSACTION for (const memo of job.memos) { if (memo.nextPhase === AcpJobPhases.TRANSACTION) { - console.log("Paying job", jobId); - await job.pay(job.price); + console.log(`Paying for job ${jobId}`); + await job.payAndAcceptRequirement(); + console.log(`Job ${jobId} paid`) } } } else if (job.phase === AcpJobPhases.REQUEST) { @@ -91,10 +94,10 @@ async function buyer() { } else if (job.phase === AcpJobPhases.TRANSACTION) { console.log(`Job ${jobId} is in TRANSACTION. Waiting for seller to deliver...`); } else if (job.phase === AcpJobPhases.COMPLETED) { - console.log("Job completed", job); + console.log(`Job ${job.id} completed, received deliverable:`, job.deliverable); finished = true; } else if (job.phase === AcpJobPhases.REJECTED) { - console.log("Job rejected", job); + console.log(`Job ${job.id} rejected`); finished = true; } } diff --git a/examples/acp-base/polling-mode/seller.ts b/examples/acp-base/polling-mode/seller.ts index 563c4fd..7387422 100644 --- a/examples/acp-base/polling-mode/seller.ts +++ b/examples/acp-base/polling-mode/seller.ts @@ -1,6 +1,7 @@ import AcpClient, { - AcpContractClient, + AcpContractClientV2, AcpJobPhases, + IDeliverable } from "@virtuals-protocol/acp-node"; import { SELLER_AGENT_WALLET_ADDRESS, @@ -12,13 +13,15 @@ import { const POLL_INTERVAL_MS = 20000; // 20 seconds // -------------------------------------------------- +const REJECT_JOB = false; + async function sleep(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); } async function seller() { const acpClient = new AcpClient({ - acpContractClient: await AcpContractClient.build( + acpContractClient: await AcpContractClientV2.build( WHITELISTED_WALLET_PRIVATE_KEY, SELLER_ENTITY_ID, SELLER_AGENT_WALLET_ADDRESS, @@ -62,21 +65,32 @@ async function seller() { console.log( `Seller: Job ${onchainJobId} is in REQUEST. Responding to buyer's request...` ); - await job.respond(true); + const response = true; + console.log(`Responding to job ${job.id} with requirement`, job.requirement); + await job.respond(response); + console.log(`Job ${job.id} responded with ${response}`); console.log(`Seller: Accepted job ${onchainJobId}. Job phase should move to NEGOTIATION.`); jobStages.responded_to_request = true; } // 2. Submit Deliverable (if job is paid and not yet delivered) else if (currentPhase === AcpJobPhases.TRANSACTION && !jobStages.delivered_work) { // Buyer has paid, job is in TRANSACTION. Seller needs to deliver. - console.log(`Seller: Job ${onchainJobId} is PAID (TRANSACTION phase). Submitting deliverable...`); - await job.deliver( - { - type: "url", - value: "https://example.com", - } - ); - console.log(`Seller: Deliverable submitted for job ${onchainJobId}. Job should move to EVALUATION.`); + // to cater cases where agent decide to reject job after payment has been made + if (REJECT_JOB) { // conditional check for job rejection logic + const reason = "Job requirement does not meet agent capability"; + console.log(`Rejecting job ${job.id} with reason: ${reason}`) + await job.respond(false, reason); + console.log(`Job ${onchainJobId} rejected`); + return; + } + + const deliverable: IDeliverable = { + type: "url", + value: "https://example.com", + } + console.log(`Delivering job ${onchainJobId} with deliverable`, deliverable); + await job.deliver(deliverable); + console.log(`Job ${onchainJobId} delivered`); jobStages.delivered_work = true; } else if ( diff --git a/examples/acp-base/self-evaluation-v2/README.md b/examples/acp-base/self-evaluation-v2/README.md new file mode 100644 index 0000000..74317e7 --- /dev/null +++ b/examples/acp-base/self-evaluation-v2/README.md @@ -0,0 +1,144 @@ +# ACP Self-Evaluation v2 Example + +This example demonstrates **ACP v2** integration flows using a buyer-seller interaction pattern with self-evaluation. + +## Overview + +This example showcases use cases enabled by ACP v2's job and payment framework: +- **Self-Evaluation**: Buyer evaluates job completion themselves +- **Job Management**: Complete job lifecycle with buyer evaluation +- **Agent Discovery**: Finding and selecting service providers +- **Simple Architecture**: Two-agent system (buyer and seller) + +## Files + +### `buyer.ts` - Service Requester +The buyer agent demonstrates how to: +- **Initiate Jobs**: Find service providers and create jobs +- **Self-Evaluation**: Evaluate job completion themselves (no external evaluator) +- **Handle Payments**: Automatic payment processing during negotiation +- **Job Monitoring**: Track job status through phases + +### `seller.ts` - Service Provider +The seller agent demonstrates how to: +- **Accept Requests**: Handle incoming job requests +- **Provide Services**: Execute requested services +- **Deliver Results**: Submit deliverables for buyer evaluation +- **Job Lifecycle**: Handle REQUEST and TRANSACTION phases + +## Setup + +1. **Environment Configuration**: + ```bash + cp .env.example .env + # Update .env with your credentials + ``` + +2. **Required Environment Variables**: + - `BUYER_AGENT_WALLET_ADDRESS`: Smart wallet address for buyer agent + - `SELLER_AGENT_WALLET_ADDRESS`: Smart wallet address for seller agent + - `BUYER_ENTITY_ID`: Session entity ID for buyer + - `SELLER_ENTITY_ID`: Session entity ID for seller + - `WHITELISTED_WALLET_PRIVATE_KEY`: Private key for whitelisted wallet + +3. **Install Dependencies**: + ```bash + npm install + ``` + +## Running the Example + +### Start the Seller (Service Provider) +```bash +cd examples/acp-base/self-evaluation-v2 +npx ts-node seller.ts +``` + +### Start the Buyer (Client) +```bash +cd examples/acp-base/self-evaluation-v2 +npx ts-node buyer.ts +``` + +## Usage Flow + +1. **Job Initiation**: Buyer searches for service providers and initiates a job with themselves as evaluator +2. **Service Provision**: Seller accepts the job request and provides the requested service +3. **Delivery**: Seller delivers the completed work/results +4. **Self-Evaluation**: Buyer evaluates their own job completion +5. **Job Completion**: Job is marked as completed based on buyer's evaluation + +## ACP v2 Features + +This example demonstrates use cases enabled by ACP v2: + +- **Self-Evaluation Workflow**: Shows how buyers can evaluate jobs themselves +- **Agent Discovery**: Finding appropriate service providers through search +- **Enhanced Job Lifecycle**: Full job flow from initiation to self-evaluation +- **Configuration Management**: Proper config handling for different environments +- **Schema Validation**: Proper handling of job offering requirements + +Note: All features are user-defined through custom job offerings. + +## Self-Evaluation Benefits + +- **Simplicity**: No need for external evaluator setup +- **Speed**: Faster completion since buyer controls evaluation +- **Control**: Buyer has full control over quality assessment +- **Cost**: Reduced complexity compared to external evaluation + +## Code Structure + +### Buyer Implementation +```typescript +const acpClient = new AcpClient({ + acpContractClient: await AcpContractClient.build( + WHITELISTED_WALLET_PRIVATE_KEY, + BUYER_ENTITY_ID, + BUYER_AGENT_WALLET_ADDRESS, + config // v2 requires config + ), + onNewTask: async (job: AcpJob, memoToSign?: AcpMemo) => { + // Handle job phases and payments + }, + onEvaluate: async (job: AcpJob) => { + // Self-evaluate job completion + await job.evaluate(true, "Self-evaluated and approved"); + } +}); +``` + +### Seller Implementation +```typescript +const acpClient = new AcpClient({ + acpContractClient: await AcpContractClient.build( + WHITELISTED_WALLET_PRIVATE_KEY, + SELLER_ENTITY_ID, + SELLER_AGENT_WALLET_ADDRESS, + config // v2 requires config + ), + onNewTask: async (job: AcpJob, memoToSign?: AcpMemo) => { + // Handle job requests and deliveries + } +}); +``` + +### Schema Requirements: +- Use object format for job initiation: `{ "": "" }` +- Replace placeholders with actual schema fields from your agent's service definition + +## Notes + +- This example uses the Base Sepolia testnet configuration +- The buyer acts as their own evaluator (self-evaluation pattern) +- Both agents must be registered and whitelisted on the ACP platform +- Replace `` with your actual search term +- Replace `` and `` with actual schema requirements + +## Troubleshooting + +- Ensure both agents are registered and whitelisted on the ACP platform +- Verify environment variables are correctly set +- Check that the seller agent is running before starting the buyer +- Monitor console output for job status updates and error messages +- Ensure job offering schema requirements are properly formatted as objects diff --git a/examples/acp-base/self-evaluation-v2/buyer.ts b/examples/acp-base/self-evaluation-v2/buyer.ts new file mode 100644 index 0000000..54b8878 --- /dev/null +++ b/examples/acp-base/self-evaluation-v2/buyer.ts @@ -0,0 +1,73 @@ +import AcpClient, { + AcpContractClientV2, + AcpJobPhases, + AcpJob, + AcpMemo, + AcpAgentSort, + AcpGraduationStatus, + AcpOnlineStatus, +} from "../../../src"; +import { + BUYER_AGENT_WALLET_ADDRESS, + WHITELISTED_WALLET_PRIVATE_KEY, + BUYER_ENTITY_ID, +} from "./env"; + +async function buyer() { + const acpClient = new AcpClient({ + acpContractClient: await AcpContractClientV2.build( + WHITELISTED_WALLET_PRIVATE_KEY, + BUYER_ENTITY_ID, + BUYER_AGENT_WALLET_ADDRESS + ), + onNewTask: async (job: AcpJob, memoToSign?: AcpMemo) => { + if ( + job.phase === AcpJobPhases.NEGOTIATION && + memoToSign?.nextPhase === AcpJobPhases.TRANSACTION + ) { + console.log(`Paying for job ${job.id}`); + await job.payAndAcceptRequirement(); + console.log(`Job ${job.id} paid`); + } else if ( + job.phase === AcpJobPhases.TRANSACTION && + memoToSign?.nextPhase === AcpJobPhases.REJECTED + ) { + console.log(`Signing job ${job.id} rejection memo, rejection reason: ${memoToSign?.content}`); + await memoToSign?.sign(true, "Accepts job rejection") + console.log(`Job ${job.id} rejection memo signed`); + } else if (job.phase === AcpJobPhases.COMPLETED) { + console.log(`Job ${job.id} completed, received deliverable:`, job.deliverable); + } else if (job.phase === AcpJobPhases.REJECTED) { + console.log(`Job ${job.id} rejected`); + } + } + }); + + // Browse available agents based on a keyword + const relevantAgents = await acpClient.browseAgents( + "", + { + sort_by: [AcpAgentSort.SUCCESSFUL_JOB_COUNT], + top_k: 5, + graduationStatus: AcpGraduationStatus.ALL, + onlineStatus: AcpOnlineStatus.ALL, + } + ); + + console.log("Relevant agents:", relevantAgents); + + // Pick one of the agents based on your criteria (in this example we just pick the first one) + const chosenAgent = relevantAgents[0]; + // Pick one of the service offerings based on your criteria (in this example we just pick the first one) + const chosenJobOffering = chosenAgent.jobOfferings[0]; // v2 uses jobOfferings instead of offerings + + const jobId = await chosenJobOffering.initiateJob( + { "": "" }, + BUYER_AGENT_WALLET_ADDRESS, // evaluator address + new Date(Date.now() + 1000 * 60 * 60 * 24) // expiredAt + ); + + console.log(`Job ${jobId} initiated`); +} + +buyer(); diff --git a/examples/acp-base/self-evaluation-v2/env.ts b/examples/acp-base/self-evaluation-v2/env.ts new file mode 100644 index 0000000..612821b --- /dev/null +++ b/examples/acp-base/self-evaluation-v2/env.ts @@ -0,0 +1,37 @@ +import dotenv from "dotenv"; +import { Address } from "viem"; + +dotenv.config({ path: __dirname + "/.env" }); + +function getEnvVar(key: string, required = true): T { + const value = process.env[key]; + if (required && (value === undefined || value === "")) { + throw new Error(`${key} is not defined or is empty in the .env file`); + } + return value as T; +} + +export const WHITELISTED_WALLET_PRIVATE_KEY = getEnvVar
( + "WHITELISTED_WALLET_PRIVATE_KEY" +); + +export const BUYER_AGENT_WALLET_ADDRESS = getEnvVar
( + "BUYER_AGENT_WALLET_ADDRESS" +); + +export const BUYER_ENTITY_ID = parseInt(getEnvVar("BUYER_ENTITY_ID")); + +export const SELLER_AGENT_WALLET_ADDRESS = getEnvVar
( + "SELLER_AGENT_WALLET_ADDRESS" +); + +export const SELLER_ENTITY_ID = parseInt(getEnvVar("SELLER_ENTITY_ID")); + +const entities = { + BUYER_ENTITY_ID, + SELLER_ENTITY_ID, +}; + +for (const [key, value] of Object.entries(entities)) { + if (isNaN(value)) throw new Error(`${key} must be a valid number`); +} diff --git a/examples/acp-base/self-evaluation-v2/seller.ts b/examples/acp-base/self-evaluation-v2/seller.ts new file mode 100644 index 0000000..b735c25 --- /dev/null +++ b/examples/acp-base/self-evaluation-v2/seller.ts @@ -0,0 +1,57 @@ +import AcpClient, { + AcpContractClientV2, + AcpJob, + AcpJobPhases, + AcpMemo, + IDeliverable +} from '../../../src'; +import { + SELLER_AGENT_WALLET_ADDRESS, + SELLER_ENTITY_ID, + WHITELISTED_WALLET_PRIVATE_KEY +} from "./env"; + +const REJECT_JOB = false; + +async function seller() { + new AcpClient({ + acpContractClient: await AcpContractClientV2.build( + WHITELISTED_WALLET_PRIVATE_KEY, + SELLER_ENTITY_ID, + SELLER_AGENT_WALLET_ADDRESS + ), + onNewTask: async (job: AcpJob, memoToSign?: AcpMemo) => { + if ( + job.phase === AcpJobPhases.REQUEST && + memoToSign?.nextPhase === AcpJobPhases.NEGOTIATION + ) { + const response = true; + console.log(`Responding to job ${job.id} with requirement`, job.requirement); + await job.respond(response); + console.log(`Job ${job.id} responded with ${response}`); + } else if ( + job.phase === AcpJobPhases.TRANSACTION && + memoToSign?.nextPhase === AcpJobPhases.EVALUATION + ) { + // to cater cases where agent decide to reject job after payment has been made + if (REJECT_JOB) { // conditional check for job rejection logic + const reason = "Job requirement does not meet agent capability"; + console.log(`Rejecting job ${job.id} with reason: ${reason}`) + await job.respond(false, reason); + console.log(`Job ${job.id} rejected`); + return; + } + + const deliverable: IDeliverable = { + type: "url", + value: "https://example.com", + } + console.log(`Delivering job ${job.id} with deliverable`, deliverable); + await job.deliver(deliverable); + console.log(`Job ${job.id} delivered`); + } + }, + }); +} + +seller(); diff --git a/examples/acp-base/self-evaluation/README.md b/examples/acp-base/self-evaluation/README.md index 583e815..80f0d39 100644 --- a/examples/acp-base/self-evaluation/README.md +++ b/examples/acp-base/self-evaluation/README.md @@ -56,7 +56,6 @@ You can customize agent discovery and job selection using: const relevantAgents = await acpClient.browseAgents( "", { - cluster: "", sort_by: [""], rerank: "", top_k: "", diff --git a/examples/acp-base/self-evaluation/buyer.ts b/examples/acp-base/self-evaluation/buyer.ts index 953fcb5..43f7002 100644 --- a/examples/acp-base/self-evaluation/buyer.ts +++ b/examples/acp-base/self-evaluation/buyer.ts @@ -42,9 +42,8 @@ async function buyer() { // Browse available agents based on a keyword and cluster name const relevantAgents = await acpClient.browseAgents( - "alpha generating agnt", + "", { - cluster: "", sort_by: [AcpAgentSort.SUCCESSFUL_JOB_COUNT], top_k: 5, graduationStatus: AcpGraduationStatus.ALL, diff --git a/examples/agent-frameworks/langchain/buyerLangchain.ts b/examples/agent-frameworks/langchain/buyerLangchain.ts index 018c9c2..46cb35c 100644 --- a/examples/agent-frameworks/langchain/buyerLangchain.ts +++ b/examples/agent-frameworks/langchain/buyerLangchain.ts @@ -161,7 +161,7 @@ async function buyer() { await job.pay(job.price); console.log(`Job ${job.id} paid`); } else if (job.phase === AcpJobPhases.COMPLETED) { - console.log(`Job ${job.id} completed with agent's decision:`, result.output); + console.log(`Job ${job.id} completed, received deliverable:`, result.output); } } catch (error) { console.error(error); @@ -184,8 +184,7 @@ async function buyer() { console.log("Agent's decision:", result.output); // Browse available agents based on the agent's decision - const relevantAgents = await acpClient.browseAgents("meme generator", { - cluster: "", + const relevantAgents = await acpClient.browseAgents("", { sort_by: [AcpAgentSort.SUCCESSFUL_JOB_COUNT], top_k: 5, graduationStatus: AcpGraduationStatus.ALL, diff --git a/src/AcpConfigs.ts b/src/AcpConfigs.ts deleted file mode 100644 index 7fb7843..0000000 --- a/src/AcpConfigs.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Address } from "@aa-sdk/core"; -import { baseSepolia, base } from "@account-kit/infra"; -import { Fare } from "./acpFare"; - -class AcpContractConfig { - constructor( - public chain: typeof baseSepolia | typeof base, - public contractAddress: Address, - public baseFare: Fare, - public alchemyRpcUrl: string, - public acpUrl: string, - public rpcEndpoint?: string - ) {} -} - -const baseSepoliaAcpConfig = new AcpContractConfig( - baseSepolia, - "0x8Db6B1c839Fc8f6bd35777E194677B67b4D51928", - new Fare("0x036CbD53842c5426634e7929541eC2318f3dCF7e", 6), - "https://alchemy-proxy.virtuals.io/api/proxy/rpc", - "https://acpx.virtuals.gg" -); - -const baseAcpConfig = new AcpContractConfig( - base, - "0x6a1FE26D54ab0d3E1e3168f2e0c0cDa5cC0A0A4A", - new Fare("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", 6), - "https://alchemy-proxy-prod.virtuals.io/api/proxy/rpc", - "https://acpx.virtuals.io" -); - -export { AcpContractConfig, baseSepoliaAcpConfig, baseAcpConfig }; diff --git a/src/acpAbi.ts b/src/abis/acpAbi.ts similarity index 100% rename from src/acpAbi.ts rename to src/abis/acpAbi.ts diff --git a/src/abis/acpAbiV2.ts b/src/abis/acpAbiV2.ts new file mode 100644 index 0000000..bd5ebfb --- /dev/null +++ b/src/abis/acpAbiV2.ts @@ -0,0 +1,824 @@ +const ACP_V2_ABI = [ + { inputs: [], stateMutability: "nonpayable", type: "constructor" }, + { inputs: [], name: "AccessControlBadConfirmation", type: "error" }, + { + inputs: [ + { internalType: "address", name: "account", type: "address" }, + { internalType: "bytes32", name: "neededRole", type: "bytes32" }, + ], + name: "AccessControlUnauthorizedAccount", + type: "error", + }, + { + inputs: [{ internalType: "address", name: "target", type: "address" }], + name: "AddressEmptyCode", + type: "error", + }, + { + inputs: [{ internalType: "address", name: "account", type: "address" }], + name: "AddressInsufficientBalance", + type: "error", + }, + { + inputs: [ + { internalType: "address", name: "implementation", type: "address" }, + ], + name: "ERC1967InvalidImplementation", + type: "error", + }, + { inputs: [], name: "ERC1967NonPayable", type: "error" }, + { inputs: [], name: "EnforcedPause", type: "error" }, + { inputs: [], name: "ExpectedPause", type: "error" }, + { inputs: [], name: "FailedInnerCall", type: "error" }, + { inputs: [], name: "InvalidInitialization", type: "error" }, + { inputs: [], name: "NotInitializing", type: "error" }, + { inputs: [], name: "ReentrancyGuardReentrantCall", type: "error" }, + { + inputs: [{ internalType: "address", name: "token", type: "address" }], + name: "SafeERC20FailedOperation", + type: "error", + }, + { inputs: [], name: "UUPSUnauthorizedCallContext", type: "error" }, + { + inputs: [{ internalType: "bytes32", name: "slot", type: "bytes32" }], + name: "UUPSUnsupportedProxiableUUID", + type: "error", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "uint256", + name: "accountId", + type: "uint256", + }, + { + indexed: true, + internalType: "address", + name: "client", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "provider", + type: "address", + }, + ], + name: "AccountCreated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "uint256", + name: "accountId", + type: "uint256", + }, + { indexed: false, internalType: "bool", name: "isActive", type: "bool" }, + ], + name: "AccountStatusUpdated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "uint256", + name: "jobId", + type: "uint256", + }, + { + indexed: true, + internalType: "address", + name: "evaluator", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "evaluatorFee", + type: "uint256", + }, + ], + name: "ClaimedEvaluatorFee", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "uint256", + name: "jobId", + type: "uint256", + }, + { + indexed: true, + internalType: "address", + name: "provider", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "providerFee", + type: "uint256", + }, + ], + name: "ClaimedProviderFee", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "uint64", + name: "version", + type: "uint64", + }, + ], + name: "Initialized", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "string", + name: "moduleType", + type: "string", + }, + { + indexed: true, + internalType: "address", + name: "oldModule", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "newModule", + type: "address", + }, + ], + name: "ModuleUpdated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "address", + name: "account", + type: "address", + }, + ], + name: "Paused", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "uint256", + name: "jobId", + type: "uint256", + }, + { + indexed: true, + internalType: "address", + name: "client", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "amount", + type: "uint256", + }, + ], + name: "RefundedBudget", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "bytes32", name: "role", type: "bytes32" }, + { + indexed: true, + internalType: "bytes32", + name: "previousAdminRole", + type: "bytes32", + }, + { + indexed: true, + internalType: "bytes32", + name: "newAdminRole", + type: "bytes32", + }, + ], + name: "RoleAdminChanged", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "bytes32", name: "role", type: "bytes32" }, + { + indexed: true, + internalType: "address", + name: "account", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "sender", + type: "address", + }, + ], + name: "RoleGranted", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "bytes32", name: "role", type: "bytes32" }, + { + indexed: true, + internalType: "address", + name: "account", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "sender", + type: "address", + }, + ], + name: "RoleRevoked", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "address", + name: "account", + type: "address", + }, + ], + name: "Unpaused", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "implementation", + type: "address", + }, + ], + name: "Upgraded", + type: "event", + }, + { + inputs: [], + name: "ADMIN_ROLE", + outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "DEFAULT_ADMIN_ROLE", + outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "MODULE_MANAGER_ROLE", + outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "OPERATOR_ROLE", + outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "UPGRADE_INTERFACE_VERSION", + outputs: [{ internalType: "string", name: "", type: "string" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "accountManager", + outputs: [ + { internalType: "contract IAccountManager", name: "", type: "address" }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "account", type: "address" }, + { internalType: "uint256", name: "jobId", type: "uint256" }, + ], + name: "canSign", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "uint256", name: "jobId", type: "uint256" }], + name: "claimBudget", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "provider", type: "address" }, + { internalType: "string", name: "metadata", type: "string" }, + ], + name: "createAccount", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "provider", type: "address" }, + { internalType: "address", name: "evaluator", type: "address" }, + { internalType: "uint256", name: "expiredAt", type: "uint256" }, + { internalType: "address", name: "paymentToken", type: "address" }, + { internalType: "uint256", name: "budget", type: "uint256" }, + { internalType: "string", name: "metadata", type: "string" }, + ], + name: "createJob", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "accountId", type: "uint256" }, + { internalType: "address", name: "evaluator", type: "address" }, + { internalType: "uint256", name: "budget", type: "uint256" }, + { internalType: "address", name: "paymentToken", type: "address" }, + { internalType: "uint256", name: "expiredAt", type: "uint256" }, + ], + name: "createJobWithAccount", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "jobId", type: "uint256" }, + { internalType: "string", name: "content", type: "string" }, + { + internalType: "enum ACPTypes.MemoType", + name: "memoType", + type: "uint8", + }, + { internalType: "bool", name: "isSecured", type: "bool" }, + { + internalType: "enum ACPTypes.JobPhase", + name: "nextPhase", + type: "uint8", + }, + ], + name: "createMemo", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "jobId", type: "uint256" }, + { internalType: "string", name: "content", type: "string" }, + { internalType: "address", name: "token", type: "address" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + { internalType: "address", name: "recipient", type: "address" }, + { internalType: "uint256", name: "feeAmount", type: "uint256" }, + { internalType: "enum ACPTypes.FeeType", name: "feeType", type: "uint8" }, + { + internalType: "enum ACPTypes.MemoType", + name: "memoType", + type: "uint8", + }, + { internalType: "uint256", name: "expiredAt", type: "uint256" }, + { internalType: "bool", name: "isSecured", type: "bool" }, + { + internalType: "enum ACPTypes.JobPhase", + name: "nextPhase", + type: "uint8", + }, + ], + name: "createPayableMemo", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "defaultPaymentToken", + outputs: [{ internalType: "contract IERC20", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "token", type: "address" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + ], + name: "emergencyWithdraw", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "evaluatorFeeBP", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "uint256", name: "accountId", type: "uint256" }], + name: "getAccount", + outputs: [ + { + components: [ + { internalType: "uint256", name: "id", type: "uint256" }, + { internalType: "address", name: "client", type: "address" }, + { internalType: "address", name: "provider", type: "address" }, + { internalType: "uint256", name: "createdAt", type: "uint256" }, + { internalType: "string", name: "metadata", type: "string" }, + { internalType: "uint256", name: "jobCount", type: "uint256" }, + { + internalType: "uint256", + name: "completedJobCount", + type: "uint256", + }, + { internalType: "bool", name: "isActive", type: "bool" }, + ], + internalType: "struct ACPTypes.Account", + name: "", + type: "tuple", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "jobId", type: "uint256" }, + { internalType: "uint256", name: "offset", type: "uint256" }, + { internalType: "uint256", name: "limit", type: "uint256" }, + ], + name: "getAllMemos", + outputs: [ + { + components: [ + { internalType: "uint256", name: "id", type: "uint256" }, + { internalType: "uint256", name: "jobId", type: "uint256" }, + { internalType: "address", name: "sender", type: "address" }, + { internalType: "string", name: "content", type: "string" }, + { + internalType: "enum ACPTypes.MemoType", + name: "memoType", + type: "uint8", + }, + { internalType: "uint256", name: "createdAt", type: "uint256" }, + { internalType: "bool", name: "isApproved", type: "bool" }, + { internalType: "address", name: "approvedBy", type: "address" }, + { internalType: "uint256", name: "approvedAt", type: "uint256" }, + { internalType: "bool", name: "requiresApproval", type: "bool" }, + { internalType: "string", name: "metadata", type: "string" }, + { internalType: "bool", name: "isSecured", type: "bool" }, + { + internalType: "enum ACPTypes.JobPhase", + name: "nextPhase", + type: "uint8", + }, + { internalType: "uint256", name: "expiredAt", type: "uint256" }, + ], + internalType: "struct ACPTypes.Memo[]", + name: "memos", + type: "tuple[]", + }, + { internalType: "uint256", name: "total", type: "uint256" }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "jobId", type: "uint256" }, + { + internalType: "enum ACPTypes.MemoType", + name: "memoType", + type: "uint8", + }, + { internalType: "uint256", name: "offset", type: "uint256" }, + { internalType: "uint256", name: "limit", type: "uint256" }, + ], + name: "getMemosForPhase", + outputs: [ + { + components: [ + { internalType: "uint256", name: "id", type: "uint256" }, + { internalType: "uint256", name: "jobId", type: "uint256" }, + { internalType: "address", name: "sender", type: "address" }, + { internalType: "string", name: "content", type: "string" }, + { + internalType: "enum ACPTypes.MemoType", + name: "memoType", + type: "uint8", + }, + { internalType: "uint256", name: "createdAt", type: "uint256" }, + { internalType: "bool", name: "isApproved", type: "bool" }, + { internalType: "address", name: "approvedBy", type: "address" }, + { internalType: "uint256", name: "approvedAt", type: "uint256" }, + { internalType: "bool", name: "requiresApproval", type: "bool" }, + { internalType: "string", name: "metadata", type: "string" }, + { internalType: "bool", name: "isSecured", type: "bool" }, + { + internalType: "enum ACPTypes.JobPhase", + name: "nextPhase", + type: "uint8", + }, + { internalType: "uint256", name: "expiredAt", type: "uint256" }, + ], + internalType: "struct ACPTypes.Memo[]", + name: "memos", + type: "tuple[]", + }, + { internalType: "uint256", name: "total", type: "uint256" }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getPhases", + outputs: [{ internalType: "string[7]", name: "", type: "string[7]" }], + stateMutability: "pure", + type: "function", + }, + { + inputs: [{ internalType: "bytes32", name: "role", type: "bytes32" }], + name: "getRoleAdmin", + outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "bytes32", name: "role", type: "bytes32" }, + { internalType: "address", name: "account", type: "address" }, + ], + name: "grantRole", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "bytes32", name: "role", type: "bytes32" }, + { internalType: "address", name: "account", type: "address" }, + ], + name: "hasRole", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "defaultPaymentToken_", + type: "address", + }, + { internalType: "uint256", name: "platformFeeBP_", type: "uint256" }, + { internalType: "address", name: "platformTreasury_", type: "address" }, + { internalType: "uint256", name: "evaluatorFeeBP_", type: "uint256" }, + ], + name: "initialize", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "jobId", type: "uint256" }, + { internalType: "address", name: "account", type: "address" }, + ], + name: "isJobEvaluator", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "jobManager", + outputs: [ + { internalType: "contract IJobManager", name: "", type: "address" }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "memoManager", + outputs: [ + { internalType: "contract IMemoManager", name: "", type: "address" }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "pause", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "paused", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "paymentManager", + outputs: [ + { internalType: "contract IPaymentManager", name: "", type: "address" }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "platformFeeBP", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "platformTreasury", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "proxiableUUID", + outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "bytes32", name: "role", type: "bytes32" }, + { internalType: "address", name: "callerConfirmation", type: "address" }, + ], + name: "renounceRole", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "bytes32", name: "role", type: "bytes32" }, + { internalType: "address", name: "account", type: "address" }, + ], + name: "revokeRole", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "jobId", type: "uint256" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + ], + name: "setBudget", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "jobId", type: "uint256" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + { internalType: "address", name: "paymentToken", type: "address" }, + ], + name: "setBudgetWithPaymentToken", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "memoId", type: "uint256" }, + { internalType: "bool", name: "isApproved", type: "bool" }, + { internalType: "string", name: "reason", type: "string" }, + ], + name: "signMemo", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "bytes4", name: "interfaceId", type: "bytes4" }], + name: "supportsInterface", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "unpause", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "accountId", type: "uint256" }, + { internalType: "string", name: "metadata", type: "string" }, + ], + name: "updateAccountMetadata", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "evaluatorFeeBP_", type: "uint256" }, + ], + name: "updateEvaluatorFee", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "string", name: "moduleType", type: "string" }, + { internalType: "address", name: "moduleAddress", type: "address" }, + ], + name: "updateModule", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "platformFeeBP_", type: "uint256" }, + { internalType: "address", name: "platformTreasury_", type: "address" }, + { internalType: "uint256", name: "evaluatorFeeBP_", type: "uint256" }, + ], + name: "updatePlatformConfig", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "newImplementation", type: "address" }, + { internalType: "bytes", name: "data", type: "bytes" }, + ], + name: "upgradeToAndCall", + outputs: [], + stateMutability: "payable", + type: "function", + }, +]; + +export default ACP_V2_ABI; diff --git a/src/abis/jobManagerAbi.ts b/src/abis/jobManagerAbi.ts new file mode 100644 index 0000000..8a8516f --- /dev/null +++ b/src/abis/jobManagerAbi.ts @@ -0,0 +1,857 @@ +const JOB_MANAGER_ABI = [ + { inputs: [], stateMutability: "nonpayable", type: "constructor" }, + { inputs: [], name: "AccessControlBadConfirmation", type: "error" }, + { + inputs: [ + { internalType: "address", name: "account", type: "address" }, + { internalType: "bytes32", name: "neededRole", type: "bytes32" }, + ], + name: "AccessControlUnauthorizedAccount", + type: "error", + }, + { + inputs: [{ internalType: "address", name: "target", type: "address" }], + name: "AddressEmptyCode", + type: "error", + }, + { + inputs: [ + { internalType: "address", name: "implementation", type: "address" }, + ], + name: "ERC1967InvalidImplementation", + type: "error", + }, + { inputs: [], name: "ERC1967NonPayable", type: "error" }, + { inputs: [], name: "FailedInnerCall", type: "error" }, + { inputs: [], name: "InvalidInitialization", type: "error" }, + { inputs: [], name: "NotInitializing", type: "error" }, + { inputs: [], name: "ReentrancyGuardReentrantCall", type: "error" }, + { inputs: [], name: "UUPSUnauthorizedCallContext", type: "error" }, + { + inputs: [{ internalType: "bytes32", name: "slot", type: "bytes32" }], + name: "UUPSUnsupportedProxiableUUID", + type: "error", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "uint256", + name: "jobId", + type: "uint256", + }, + { + indexed: false, + internalType: "uint256", + name: "newBudget", + type: "uint256", + }, + ], + name: "BudgetSet", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "uint64", + name: "version", + type: "uint64", + }, + ], + name: "Initialized", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "uint256", + name: "jobId", + type: "uint256", + }, + { + indexed: true, + internalType: "address", + name: "oldAssignee", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "newAssignee", + type: "address", + }, + ], + name: "JobAssigned", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "uint256", + name: "jobId", + type: "uint256", + }, + { + indexed: true, + internalType: "uint256", + name: "accountId", + type: "uint256", + }, + { + indexed: true, + internalType: "address", + name: "client", + type: "address", + }, + { + indexed: false, + internalType: "address", + name: "provider", + type: "address", + }, + { + indexed: false, + internalType: "address", + name: "evaluator", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "expiredAt", + type: "uint256", + }, + ], + name: "JobCreated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "uint256", + name: "jobId", + type: "uint256", + }, + { + indexed: true, + internalType: "uint256", + name: "dependencyJobId", + type: "uint256", + }, + ], + name: "JobDependencyAdded", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "uint256", + name: "jobId", + type: "uint256", + }, + { + indexed: true, + internalType: "uint256", + name: "dependencyJobId", + type: "uint256", + }, + ], + name: "JobDependencyRemoved", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "uint256", + name: "jobId", + type: "uint256", + }, + { + indexed: true, + internalType: "address", + name: "paymentToken", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "newBudget", + type: "uint256", + }, + ], + name: "JobPaymentTokenSet", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "uint256", + name: "jobId", + type: "uint256", + }, + { + indexed: false, + internalType: "enum ACPTypes.JobPhase", + name: "oldPhase", + type: "uint8", + }, + { + indexed: false, + internalType: "enum ACPTypes.JobPhase", + name: "newPhase", + type: "uint8", + }, + ], + name: "JobPhaseUpdated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "uint256", + name: "jobId", + type: "uint256", + }, + { + indexed: false, + internalType: "enum ACPTypes.JobStatus", + name: "oldStatus", + type: "uint8", + }, + { + indexed: false, + internalType: "enum ACPTypes.JobStatus", + name: "newStatus", + type: "uint8", + }, + { + indexed: false, + internalType: "address", + name: "updatedBy", + type: "address", + }, + ], + name: "JobStatusUpdated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "bytes32", name: "role", type: "bytes32" }, + { + indexed: true, + internalType: "bytes32", + name: "previousAdminRole", + type: "bytes32", + }, + { + indexed: true, + internalType: "bytes32", + name: "newAdminRole", + type: "bytes32", + }, + ], + name: "RoleAdminChanged", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "bytes32", name: "role", type: "bytes32" }, + { + indexed: true, + internalType: "address", + name: "account", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "sender", + type: "address", + }, + ], + name: "RoleGranted", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "bytes32", name: "role", type: "bytes32" }, + { + indexed: true, + internalType: "address", + name: "account", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "sender", + type: "address", + }, + ], + name: "RoleRevoked", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "implementation", + type: "address", + }, + ], + name: "Upgraded", + type: "event", + }, + { + inputs: [], + name: "ACP_CONTRACT_ROLE", + outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "ADMIN_ROLE", + outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "DEFAULT_ADMIN_ROLE", + outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "MEMO_MANAGER_ROLE", + outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "UPGRADE_INTERFACE_VERSION", + outputs: [{ internalType: "string", name: "", type: "string" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "", type: "uint256" }, + { internalType: "uint256", name: "", type: "uint256" }, + ], + name: "accountJobs", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "acpContract", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "jobId", type: "uint256" }, + { internalType: "uint256", name: "dependencyJobId", type: "uint256" }, + ], + name: "addJobDependency", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "jobId", type: "uint256" }, + { internalType: "address", name: "assignee", type: "address" }, + ], + name: "assignJob", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "uint256[]", name: "jobIds", type: "uint256[]" }, + { + internalType: "enum ACPTypes.JobStatus", + name: "newStatus", + type: "uint8", + }, + ], + name: "bulkUpdateJobStatus", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "jobId", type: "uint256" }, + { internalType: "address", name: "user", type: "address" }, + ], + name: "canModifyJob", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "uint256", name: "jobId", type: "uint256" }], + name: "canStartJob", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "accountId", type: "uint256" }, + { internalType: "address", name: "client", type: "address" }, + { internalType: "address", name: "provider", type: "address" }, + { internalType: "address", name: "evaluator", type: "address" }, + { internalType: "address", name: "creator", type: "address" }, + { internalType: "uint256", name: "budget", type: "uint256" }, + { + internalType: "contract IERC20", + name: "paymentToken", + type: "address", + }, + { internalType: "uint256", name: "expiredAt", type: "uint256" }, + ], + name: "createJob", + outputs: [{ internalType: "uint256", name: "jobId", type: "uint256" }], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "uint256", name: "accountId", type: "uint256" }], + name: "getAccountJobStats", + outputs: [ + { internalType: "uint256", name: "totalJobs", type: "uint256" }, + { internalType: "uint256", name: "completedJobs", type: "uint256" }, + { internalType: "uint256", name: "inProgressJobs", type: "uint256" }, + { internalType: "uint256", name: "pendingJobs", type: "uint256" }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "accountId", type: "uint256" }, + { internalType: "uint256", name: "offset", type: "uint256" }, + { internalType: "uint256", name: "limit", type: "uint256" }, + ], + name: "getAccountJobs", + outputs: [ + { + components: [ + { internalType: "uint256", name: "id", type: "uint256" }, + { internalType: "uint256", name: "accountId", type: "uint256" }, + { internalType: "address", name: "client", type: "address" }, + { internalType: "address", name: "provider", type: "address" }, + { internalType: "address", name: "evaluator", type: "address" }, + { internalType: "address", name: "creator", type: "address" }, + { internalType: "address", name: "assignee", type: "address" }, + { internalType: "uint256", name: "budget", type: "uint256" }, + { + internalType: "contract IERC20", + name: "paymentToken", + type: "address", + }, + { + internalType: "enum ACPTypes.JobPhase", + name: "phase", + type: "uint8", + }, + { + internalType: "enum ACPTypes.JobStatus", + name: "status", + type: "uint8", + }, + { internalType: "uint256", name: "expiredAt", type: "uint256" }, + { internalType: "uint256", name: "createdAt", type: "uint256" }, + { internalType: "uint256", name: "startedAt", type: "uint256" }, + { internalType: "uint256", name: "completedAt", type: "uint256" }, + { internalType: "uint256", name: "memoCount", type: "uint256" }, + { internalType: "string", name: "metadata", type: "string" }, + { + internalType: "uint256[]", + name: "dependencies", + type: "uint256[]", + }, + ], + internalType: "struct ACPTypes.Job[]", + name: "jobArray", + type: "tuple[]", + }, + { internalType: "uint256", name: "total", type: "uint256" }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "uint256", name: "jobId", type: "uint256" }], + name: "getJob", + outputs: [ + { + components: [ + { internalType: "uint256", name: "id", type: "uint256" }, + { internalType: "uint256", name: "accountId", type: "uint256" }, + { internalType: "address", name: "client", type: "address" }, + { internalType: "address", name: "provider", type: "address" }, + { internalType: "address", name: "evaluator", type: "address" }, + { internalType: "address", name: "creator", type: "address" }, + { internalType: "address", name: "assignee", type: "address" }, + { internalType: "uint256", name: "budget", type: "uint256" }, + { + internalType: "contract IERC20", + name: "paymentToken", + type: "address", + }, + { + internalType: "enum ACPTypes.JobPhase", + name: "phase", + type: "uint8", + }, + { + internalType: "enum ACPTypes.JobStatus", + name: "status", + type: "uint8", + }, + { internalType: "uint256", name: "expiredAt", type: "uint256" }, + { internalType: "uint256", name: "createdAt", type: "uint256" }, + { internalType: "uint256", name: "startedAt", type: "uint256" }, + { internalType: "uint256", name: "completedAt", type: "uint256" }, + { internalType: "uint256", name: "memoCount", type: "uint256" }, + { internalType: "string", name: "metadata", type: "string" }, + { + internalType: "uint256[]", + name: "dependencies", + type: "uint256[]", + }, + ], + internalType: "struct ACPTypes.Job", + name: "", + type: "tuple", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "uint256", name: "jobId", type: "uint256" }], + name: "getJobDependencies", + outputs: [{ internalType: "uint256[]", name: "", type: "uint256[]" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "bytes32", name: "role", type: "bytes32" }], + name: "getRoleAdmin", + outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "assignee", type: "address" }, + { internalType: "uint256", name: "offset", type: "uint256" }, + { internalType: "uint256", name: "limit", type: "uint256" }, + ], + name: "getUserJobs", + outputs: [ + { + components: [ + { internalType: "uint256", name: "id", type: "uint256" }, + { internalType: "uint256", name: "accountId", type: "uint256" }, + { internalType: "address", name: "client", type: "address" }, + { internalType: "address", name: "provider", type: "address" }, + { internalType: "address", name: "evaluator", type: "address" }, + { internalType: "address", name: "creator", type: "address" }, + { internalType: "address", name: "assignee", type: "address" }, + { internalType: "uint256", name: "budget", type: "uint256" }, + { + internalType: "contract IERC20", + name: "paymentToken", + type: "address", + }, + { + internalType: "enum ACPTypes.JobPhase", + name: "phase", + type: "uint8", + }, + { + internalType: "enum ACPTypes.JobStatus", + name: "status", + type: "uint8", + }, + { internalType: "uint256", name: "expiredAt", type: "uint256" }, + { internalType: "uint256", name: "createdAt", type: "uint256" }, + { internalType: "uint256", name: "startedAt", type: "uint256" }, + { internalType: "uint256", name: "completedAt", type: "uint256" }, + { internalType: "uint256", name: "memoCount", type: "uint256" }, + { internalType: "string", name: "metadata", type: "string" }, + { + internalType: "uint256[]", + name: "dependencies", + type: "uint256[]", + }, + ], + internalType: "struct ACPTypes.Job[]", + name: "jobArray", + type: "tuple[]", + }, + { internalType: "uint256", name: "total", type: "uint256" }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "bytes32", name: "role", type: "bytes32" }, + { internalType: "address", name: "account", type: "address" }, + ], + name: "grantRole", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "bytes32", name: "role", type: "bytes32" }, + { internalType: "address", name: "account", type: "address" }, + ], + name: "hasRole", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "uint256", name: "jobId", type: "uint256" }], + name: "incrementMemoCount", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "acpContract_", type: "address" }, + ], + name: "initialize", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "jobCounter", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "", type: "uint256" }, + { internalType: "uint256", name: "", type: "uint256" }, + ], + name: "jobDependencies", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "", type: "uint256" }, + { internalType: "uint256", name: "", type: "uint256" }, + ], + name: "jobDependents", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "uint256", name: "", type: "uint256" }], + name: "jobs", + outputs: [ + { internalType: "uint256", name: "id", type: "uint256" }, + { internalType: "uint256", name: "accountId", type: "uint256" }, + { internalType: "address", name: "client", type: "address" }, + { internalType: "address", name: "provider", type: "address" }, + { internalType: "address", name: "evaluator", type: "address" }, + { internalType: "address", name: "creator", type: "address" }, + { internalType: "address", name: "assignee", type: "address" }, + { internalType: "uint256", name: "budget", type: "uint256" }, + { + internalType: "contract IERC20", + name: "paymentToken", + type: "address", + }, + { internalType: "enum ACPTypes.JobPhase", name: "phase", type: "uint8" }, + { + internalType: "enum ACPTypes.JobStatus", + name: "status", + type: "uint8", + }, + { internalType: "uint256", name: "expiredAt", type: "uint256" }, + { internalType: "uint256", name: "createdAt", type: "uint256" }, + { internalType: "uint256", name: "startedAt", type: "uint256" }, + { internalType: "uint256", name: "completedAt", type: "uint256" }, + { internalType: "uint256", name: "memoCount", type: "uint256" }, + { internalType: "string", name: "metadata", type: "string" }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "jobId", type: "uint256" }, + { internalType: "address", name: "completedBy", type: "address" }, + ], + name: "markJobCompleted", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "proxiableUUID", + outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "jobId", type: "uint256" }, + { internalType: "uint256", name: "dependencyJobId", type: "uint256" }, + ], + name: "removeJobDependency", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "bytes32", name: "role", type: "bytes32" }, + { internalType: "address", name: "callerConfirmation", type: "address" }, + ], + name: "renounceRole", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "bytes32", name: "role", type: "bytes32" }, + { internalType: "address", name: "account", type: "address" }, + ], + name: "revokeRole", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "jobId", type: "uint256" }, + { internalType: "uint256", name: "budget", type: "uint256" }, + { + internalType: "contract IERC20", + name: "paymentToken", + type: "address", + }, + ], + name: "setJobBudget", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "bytes4", name: "interfaceId", type: "bytes4" }], + name: "supportsInterface", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "acpContract_", type: "address" }, + ], + name: "updateContracts", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "jobId", type: "uint256" }, + { + internalType: "enum ACPTypes.JobPhase", + name: "newPhase", + type: "uint8", + }, + ], + name: "updateJobPhase", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "jobId", type: "uint256" }, + { + internalType: "enum ACPTypes.JobStatus", + name: "newStatus", + type: "uint8", + }, + ], + name: "updateJobStatus", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "newImplementation", type: "address" }, + { internalType: "bytes", name: "data", type: "bytes" }, + ], + name: "upgradeToAndCall", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "", type: "address" }, + { internalType: "uint256", name: "", type: "uint256" }, + ], + name: "userJobs", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, +]; + +export default JOB_MANAGER_ABI; diff --git a/src/wethAbi.ts b/src/abis/wethAbi.ts similarity index 100% rename from src/wethAbi.ts rename to src/abis/wethAbi.ts diff --git a/src/acpAccount.ts b/src/acpAccount.ts new file mode 100644 index 0000000..127fade --- /dev/null +++ b/src/acpAccount.ts @@ -0,0 +1,21 @@ +import { Address } from "viem"; +import BaseAcpContractClient from "./contractClients/baseAcpContractClient"; + +export class AcpAccount { + constructor( + public contractClient: BaseAcpContractClient, + public id: number, + public clientAddress: Address, + public providerAddress: Address, + public metadata: Record + ) {} + + async updateMetadata(metadata: Record) { + const hash = await this.contractClient.updateAccountMetadata( + this.id, + JSON.stringify(metadata) + ); + + return hash; + } +} diff --git a/src/acpClient.ts b/src/acpClient.ts index 856a2b5..e5e5b1a 100644 --- a/src/acpClient.ts +++ b/src/acpClient.ts @@ -1,10 +1,10 @@ import { Address } from "viem"; import { io } from "socket.io-client"; -import AcpContractClient, { +import BaseAcpContractClient, { AcpJobPhases, FeeType, MemoType, -} from "./acpContractClient"; +} from "./contractClients/baseAcpContractClient"; import AcpJob from "./acpJob"; import AcpMemo from "./acpMemo"; import AcpJobOffering from "./acpJobOffering"; @@ -14,6 +14,7 @@ import { AcpGraduationStatus, AcpOnlineStatus, GenericPayload, + IAcpAccount, IAcpClientOptions, IAcpJob, IAcpJobResponse, @@ -21,8 +22,16 @@ import { IDeliverable, PayableDetails, } from "./interfaces"; -import { ethFare, FareBigInt, IFareAmount, wethFare } from "./acpFare"; import AcpError from "./acpError"; +import { + ethFare, + FareAmount, + FareAmountBase, + FareBigInt, + wethFare, +} from "./acpFare"; +import { AcpAccount } from "./acpAccount"; +import { baseAcpConfig, baseSepoliaAcpConfig } from "./configs/acpConfigs"; import { tryParseJson } from "./utils"; const { version } = require("../package.json"); @@ -51,35 +60,78 @@ export class EvaluateResult { } class AcpClient { - private acpUrl; - public acpContractClient: AcpContractClient; + private contractClients: BaseAcpContractClient[]; private onNewTask?: (job: AcpJob, memoToSign?: AcpMemo) => void; private onEvaluate?: (job: AcpJob) => void; constructor(options: IAcpClientOptions) { - this.acpContractClient = options.acpContractClient; + this.contractClients = Array.isArray(options.acpContractClient) + ? options.acpContractClient + : [options.acpContractClient]; + + if (this.contractClients.length === 0) { + throw new AcpError("ACP contract client is required"); + } + + this.contractClients.every((client) => { + if (client.contractAddress !== this.contractClients[0].contractAddress) { + throw new AcpError( + "All contract clients must have the same agent wallet address" + ); + } + }); + this.onNewTask = options.onNewTask; this.onEvaluate = options.onEvaluate || this.defaultOnEvaluate; - this.acpUrl = this.acpContractClient.config.acpUrl; this.init(); } + public contractClientByAddress(address: Address | undefined) { + if (!address) { + return this.contractClients[0]; + } + + const result = this.contractClients.find( + (client) => client.contractAddress === address + ); + + if (!result) { + throw new AcpError("ACP contract client not found"); + } + + return result; + } + + get acpContractClient() { + return this.contractClients[0]; + } + + get acpUrl() { + return this.acpContractClient.config.acpUrl; + } + private async defaultOnEvaluate(job: AcpJob) { await job.evaluate(true, "Evaluated by default"); } + get walletAddress() { + // always prioritize the first client + if (Array.isArray(this.acpContractClient)) { + return this.acpContractClient[0].walletAddress; + } + return this.acpContractClient.walletAddress; + } + async init() { const socket = io(this.acpUrl, { auth: { - walletAddress: this.acpContractClient.walletAddress, - ...(this.onEvaluate !== this.defaultOnEvaluate && { - evaluatorAddress: this.acpContractClient.walletAddress, - }), + walletAddress: this.walletAddress, }, extraHeaders: { "x-sdk-version": version, "x-sdk-language": "node", + "x-contract-address": this.contractClients[0].contractAddress, // always prioritize the first client }, transports: ["websocket"], }); @@ -105,7 +157,7 @@ class AcpClient { data.priceTokenAddress, data.memos.map((memo) => { return new AcpMemo( - this, + this.contractClientByAddress(data.contractAddress), memo.id, memo.memoType, memo.content, @@ -120,7 +172,8 @@ class AcpClient { ); }), data.phase, - data.context + data.context, + data.contractAddress ); this.onEvaluate(job); @@ -144,7 +197,7 @@ class AcpClient { data.priceTokenAddress, data.memos.map((memo) => { return new AcpMemo( - this, + this.contractClientByAddress(data.contractAddress), memo.id, memo.memoType, memo.content, @@ -159,7 +212,8 @@ class AcpClient { ); }), data.phase, - data.context + data.context, + data.contractAddress ); this.onNewTask( @@ -184,7 +238,7 @@ class AcpClient { let { cluster, sort_by, top_k, graduationStatus, onlineStatus } = options; top_k = top_k ?? 5; - let url = `${this.acpUrl}/api/agents/v2/search?search=${keyword}`; + let url = `${this.acpUrl}/api/agents/v3/search?search=${keyword}`; if (sort_by && sort_by.length > 0) { url += `&sortBy=${sort_by.map((s) => s).join(",")}`; @@ -194,8 +248,8 @@ class AcpClient { url += `&top_k=${top_k}`; } - if (this.acpContractClient.walletAddress) { - url += `&walletAddressesToExclude=${this.acpContractClient.walletAddress}`; + if (this.walletAddress) { + url += `&walletAddressesToExclude=${this.walletAddress}`; } if (cluster) { @@ -215,57 +269,97 @@ class AcpClient { data: AcpAgent[]; } = await response.json(); - return data.data.map((agent) => { - return { - id: agent.id, - name: agent.name, - description: agent.description, - offerings: agent.offerings.map((offering) => { - return new AcpJobOffering( - this, - agent.walletAddress, - offering.name, - offering.priceUsd, - offering.requirementSchema - ); - }), - twitterHandle: agent.twitterHandle, - walletAddress: agent.walletAddress, - metrics: agent.metrics, - }; - }); + const availableContractClientAddresses = this.contractClients.map( + (client) => client.contractAddress.toLowerCase() + ); + + return data.data + .filter( + (agent) => + agent.walletAddress.toLowerCase() !== this.walletAddress.toLowerCase() + ) + .filter((agent) => + availableContractClientAddresses.includes( + agent.contractAddress.toLowerCase() + ) + ) + .map((agent) => { + const acpContractClient = this.contractClients.find( + (client) => + client.contractAddress.toLowerCase() === + agent.contractAddress.toLowerCase() + ); + + if (!acpContractClient) { + throw new AcpError("ACP contract client not found"); + } + + return { + id: agent.id, + name: agent.name, + description: agent.description, + jobOfferings: agent.jobs.map((jobs) => { + return new AcpJobOffering( + this, + acpContractClient, + agent.walletAddress, + jobs.name, + jobs.price, + jobs.requirement + ); + }), + contractAddress: agent.contractAddress, + twitterHandle: agent.twitterHandle, + walletAddress: agent.walletAddress, + metrics: agent.metrics, + }; + }); } async initiateJob( providerAddress: Address, serviceRequirement: Object | string, - fareAmount: IFareAmount, + fareAmount: FareAmountBase, evaluatorAddress?: Address, expiredAt: Date = new Date(Date.now() + 1000 * 60 * 60 * 24) ) { - if (providerAddress === this.acpContractClient.walletAddress) { + if (providerAddress === this.walletAddress) { throw new AcpError( "Provider address cannot be the same as the client address" ); } - const { jobId } = await this.acpContractClient.createJob( + const account = await this.getByClientAndProvider( + this.walletAddress, providerAddress, - evaluatorAddress || this.acpContractClient.walletAddress, - expiredAt + this.acpContractClient ); - await this.acpContractClient.setBudgetWithPaymentToken( - jobId, - fareAmount.amount, - fareAmount.fare.contractAddress - ); + const { jobId, txHash } = + [ + baseSepoliaAcpConfig.contractAddress, + baseAcpConfig.contractAddress, + ].includes(this.acpContractClient.config.contractAddress) || !account + ? await this.acpContractClient.createJob( + providerAddress, + evaluatorAddress || this.walletAddress, + expiredAt, + fareAmount.fare.contractAddress, + fareAmount.amount, + "" + ) + : await this.acpContractClient.createJobWithAccount( + account.id, + providerAddress, + evaluatorAddress || this.walletAddress, + fareAmount.amount, + fareAmount.fare.contractAddress, + expiredAt + ); await this.acpContractClient.createMemo( jobId, - typeof serviceRequirement === "string" - ? serviceRequirement - : JSON.stringify(serviceRequirement), + JSON.stringify(serviceRequirement), MemoType.MESSAGE, true, AcpJobPhases.NEGOTIATION @@ -274,6 +368,48 @@ class AcpClient { return jobId; } + async createMemo(jobId: number, content: string, nextPhase: AcpJobPhases) { + return await this.acpContractClient.createMemo( + jobId, + content, + MemoType.MESSAGE, + false, + nextPhase + ); + } + + async createPayableMemo( + jobId: number, + content: string, + amount: FareAmountBase, + recipient: Address, + nextPhase: AcpJobPhases, + type: MemoType.PAYABLE_REQUEST | MemoType.PAYABLE_TRANSFER_ESCROW, + expiredAt: Date + ) { + if (type === MemoType.PAYABLE_TRANSFER_ESCROW) { + await this.acpContractClient.approveAllowance( + amount.amount, + amount.fare.contractAddress + ); + } + + const feeAmount = new FareAmount(0, this.acpContractClient.config.baseFare); + + return await this.acpContractClient.createPayableMemo( + jobId, + content, + amount.amount, + recipient, + feeAmount.amount, + FeeType.NO_FEE, + nextPhase, + type, + expiredAt, + amount.fare.contractAddress + ); + } + async respondJob( jobId: number, memoId: number, @@ -319,9 +455,9 @@ class AcpClient { async requestFunds( jobId: number, - transferFareAmount: IFareAmount, + transferFareAmount: FareAmountBase, recipient: Address, - feeFareAmount: IFareAmount, + feeFareAmount: FareAmountBase, feeType: FeeType, reason: GenericPayload, nextPhase: AcpJobPhases, @@ -359,9 +495,9 @@ class AcpClient { async transferFunds( jobId: number, - transferFareAmount: IFareAmount, + transferFareAmount: FareAmountBase, recipient: Address, - feeFareAmount: IFareAmount, + feeFareAmount: FareAmountBase, feeType: FeeType, reason: GenericPayload, nextPhase: AcpJobPhases, @@ -436,6 +572,16 @@ class AcpClient { return await this.acpContractClient.signMemo(memoId, accept, reason); } + async rejectJob(jobId: number, reason?: string) { + return await this.acpContractClient.createMemo( + jobId, + `Job ${jobId} rejected. ${reason || ""}`, + MemoType.MESSAGE, + false, + AcpJobPhases.REJECTED + ); + } + async deliverJob(jobId: number, deliverable: IDeliverable) { return await this.acpContractClient.createMemo( jobId, @@ -452,7 +598,7 @@ class AcpClient { try { const response = await fetch(url, { headers: { - "wallet-address": this.acpContractClient.walletAddress, + "wallet-address": this.walletAddress, }, }); @@ -473,7 +619,7 @@ class AcpClient { job.priceTokenAddress, job.memos.map((memo) => { return new AcpMemo( - this, + this.contractClientByAddress(job.contractAddress), memo.id, memo.memoType, memo.content, @@ -486,7 +632,8 @@ class AcpClient { ); }), job.phase, - job.context + job.context, + job.contractAddress ); }); } catch (error) { @@ -521,7 +668,7 @@ class AcpClient { job.priceTokenAddress, job.memos.map((memo) => { return new AcpMemo( - this, + this.contractClientByAddress(job.contractAddress), memo.id, memo.memoType, memo.content, @@ -536,7 +683,8 @@ class AcpClient { ); }), job.phase, - job.context + job.context, + job.contractAddress ); }); } catch (error) { @@ -571,7 +719,7 @@ class AcpClient { job.priceTokenAddress, job.memos.map((memo) => { return new AcpMemo( - this, + this.contractClientByAddress(job.contractAddress), memo.id, memo.memoType, memo.content, @@ -584,7 +732,8 @@ class AcpClient { ); }), job.phase, - job.context + job.context, + job.contractAddress ); }); } catch (error) { @@ -598,7 +747,7 @@ class AcpClient { try { const response = await fetch(url, { headers: { - "wallet-address": this.acpContractClient.walletAddress, + "wallet-address": this.walletAddress, }, }); @@ -618,7 +767,7 @@ class AcpClient { job.priceTokenAddress, job.memos.map((memo) => { return new AcpMemo( - this, + this.contractClientByAddress(job.contractAddress), memo.id, memo.memoType, memo.content, @@ -631,7 +780,8 @@ class AcpClient { ); }), job.phase, - job.context + job.context, + job.contractAddress ); }); } catch (error) { @@ -670,7 +820,7 @@ class AcpClient { job.priceTokenAddress, job.memos.map((memo) => { return new AcpMemo( - this, + this.contractClientByAddress(job.contractAddress), memo.id, memo.memoType, memo.content, @@ -683,7 +833,8 @@ class AcpClient { ); }), job.phase, - job.context + job.context, + job.contractAddress ); } catch (error) { throw new AcpError("Failed to get job by id", error); @@ -696,7 +847,7 @@ class AcpClient { try { const response = await fetch(url, { headers: { - "wallet-address": this.acpContractClient.walletAddress, + "wallet-address": this.walletAddress, }, }); @@ -712,7 +863,7 @@ class AcpClient { } return new AcpMemo( - this, + this.contractClientByAddress(memo.contractAddress), memo.id, memo.memoType, memo.content, @@ -744,6 +895,63 @@ class AcpClient { return agents[0]; } + + async getAccountByJobId( + jobId: number, + acpContractClient?: BaseAcpContractClient + ) { + try { + const url = `${this.acpUrl}/api/accounts/job/${jobId}`; + + const response = await fetch(url); + const data: { + data: IAcpAccount; + } = await response.json(); + + if (!data.data) { + return null; + } + + return new AcpAccount( + acpContractClient || this.contractClients[0], + data.data.id, + data.data.clientAddress, + data.data.providerAddress, + data.data.metadata + ); + } catch (error) { + throw new AcpError("Failed to get account by job id", error); + } + } + + async getByClientAndProvider( + clientAddress: Address, + providerAddress: Address, + acpContractClient?: BaseAcpContractClient + ) { + try { + const url = `${this.acpUrl}/api/accounts/client/${clientAddress}/provider/${providerAddress}`; + + const response = await fetch(url); + const data: { + data: IAcpAccount; + } = await response.json(); + + if (!data.data) { + return null; + } + + return new AcpAccount( + acpContractClient || this.contractClients[0], + data.data.id, + data.data.clientAddress, + data.data.providerAddress, + data.data.metadata + ); + } catch (error) { + throw new AcpError("Failed to get account by client and provider", error); + } + } } export default AcpClient; diff --git a/src/acpContractClient.ts b/src/acpContractClient.ts deleted file mode 100644 index 1d14b8a..0000000 --- a/src/acpContractClient.ts +++ /dev/null @@ -1,392 +0,0 @@ -import { Address, LocalAccountSigner, SmartAccountSigner } from "@aa-sdk/core"; -import { alchemy } from "@account-kit/infra"; -import { - ModularAccountV2Client, - createModularAccountV2Client, -} from "@account-kit/smart-contracts"; -import ACP_ABI from "./acpAbi"; -import { decodeEventLog, encodeFunctionData, erc20Abi, fromHex } from "viem"; -import { AcpContractConfig, baseAcpConfig } from "./acpConfigs"; -import WETH_ABI from "./wethAbi"; -import { wethFare } from "./acpFare"; -import AcpError from "./acpError"; - -export enum MemoType { - MESSAGE, - CONTEXT_URL, - IMAGE_URL, - VOICE_URL, - OBJECT_URL, - TXHASH, - PAYABLE_REQUEST, - PAYABLE_TRANSFER, - PAYABLE_TRANSFER_ESCROW, -} - -export enum AcpJobPhases { - REQUEST = 0, - NEGOTIATION = 1, - TRANSACTION = 2, - EVALUATION = 3, - COMPLETED = 4, - REJECTED = 5, - EXPIRED = 6, -} - -export enum FeeType { - NO_FEE, - IMMEDIATE_FEE, - DEFERRED_FEE, -} - -class AcpContractClient { - private MAX_RETRIES = 3; - private PRIORITY_FEE_MULTIPLIER = 2; - private MAX_FEE_PER_GAS = 20000000; - private MAX_PRIORITY_FEE_PER_GAS = 21000000; - - private _sessionKeyClient: ModularAccountV2Client | undefined; - private chain; - private contractAddress: Address; - - constructor( - private walletPrivateKey: Address, - private sessionEntityKeyId: number, - private agentWalletAddress: Address, - public config: AcpContractConfig = baseAcpConfig - ) { - this.chain = config.chain; - this.contractAddress = config.contractAddress; - } - - static async build( - walletPrivateKey: Address, - sessionEntityKeyId: number, - agentWalletAddress: Address, - config: AcpContractConfig = baseAcpConfig - ) { - const acpContractClient = new AcpContractClient( - walletPrivateKey, - sessionEntityKeyId, - agentWalletAddress, - config - ); - - await acpContractClient.init(); - - return acpContractClient; - } - - async init() { - const sessionKeySigner: SmartAccountSigner = - LocalAccountSigner.privateKeyToAccountSigner(this.walletPrivateKey); - - this._sessionKeyClient = await createModularAccountV2Client({ - chain: this.chain, - transport: alchemy({ - rpcUrl: this.config.alchemyRpcUrl, - }), - signer: sessionKeySigner, - policyId: "186aaa4a-5f57-4156-83fb-e456365a8820", - accountAddress: this.agentWalletAddress, - signerEntity: { - entityId: this.sessionEntityKeyId, - isGlobalValidation: true, - }, - }); - } - - getRandomNonce(bits = 152) { - const bytes = bits / 8; - const array = new Uint8Array(bytes); - crypto.getRandomValues(array); - - let hex = Array.from(array, (b) => b.toString(16).padStart(2, "0")).join( - "" - ); - return BigInt("0x" + hex); - } - - get sessionKeyClient() { - if (!this._sessionKeyClient) { - throw new AcpError("Session key client not initialized"); - } - - return this._sessionKeyClient; - } - - get walletAddress() { - return this.sessionKeyClient.account.address as Address; - } - - private async calculateGasFees() { - const finalMaxFeePerGas = - BigInt(this.MAX_FEE_PER_GAS) + - BigInt(this.MAX_PRIORITY_FEE_PER_GAS) * - BigInt(Math.max(0, this.PRIORITY_FEE_MULTIPLIER - 1)); - - return finalMaxFeePerGas; - } - - private async handleSendUserOperation( - data: `0x${string}`, - contractAddress: Address = this.contractAddress, - value?: bigint - ) { - const payload: any = { - uo: { - target: contractAddress, - data: data, - value: value, - }, - overrides: { - nonceKey: this.getRandomNonce(), - }, - }; - - let retries = this.MAX_RETRIES; - let finalError: unknown; - - while (retries > 0) { - try { - if (this.MAX_RETRIES > retries) { - const gasFees = await this.calculateGasFees(); - - payload["overrides"] = { - maxFeePerGas: `0x${gasFees.toString(16)}`, - }; - } - - const { hash } = await this.sessionKeyClient.sendUserOperation(payload); - - await this.sessionKeyClient.waitForUserOperationTransaction({ - hash, - }); - - return hash; - } catch (error) { - retries -= 1; - if (retries === 0) { - finalError = error; - break; - } - - await new Promise((resolve) => setTimeout(resolve, 2000 * retries)); - } - } - - throw new AcpError(`Failed to send user operation`, finalError); - } - - private async getJobId(hash: Address) { - const result = await this.sessionKeyClient.getUserOperationReceipt(hash); - - if (!result) { - throw new AcpError("Failed to get user operation receipt"); - } - - const contractLogs = result.logs.find( - (log: any) => - log.address.toLowerCase() === this.contractAddress.toLowerCase() - ) as any; - - if (!contractLogs) { - throw new AcpError("Failed to get contract logs"); - } - - return fromHex(contractLogs.data, "number"); - } - - async createJob( - providerAddress: string, - evaluatorAddress: string, - expireAt: Date - ): Promise<{ txHash: string; jobId: number }> { - try { - const data = encodeFunctionData({ - abi: ACP_ABI, - functionName: "createJob", - args: [ - providerAddress, - evaluatorAddress, - Math.floor(expireAt.getTime() / 1000), - ], - }); - - const hash = await this.handleSendUserOperation(data); - - const jobId = await this.getJobId(hash); - - return { txHash: hash, jobId: jobId }; - } catch (error) { - throw new AcpError("Failed to create job", error); - } - } - - async approveAllowance( - amountBaseUnit: bigint, - paymentTokenAddress: Address = this.config.baseFare.contractAddress - ) { - try { - const data = encodeFunctionData({ - abi: erc20Abi, - functionName: "approve", - args: [this.contractAddress, amountBaseUnit], - }); - - return await this.handleSendUserOperation(data, paymentTokenAddress); - } catch (error) { - throw new AcpError("Failed to approve allowance", error); - } - } - - async createPayableMemo( - jobId: number, - content: string, - amountBaseUnit: bigint, - recipient: Address, - feeAmountBaseUnit: bigint, - feeType: FeeType, - nextPhase: AcpJobPhases, - type: MemoType.PAYABLE_REQUEST | MemoType.PAYABLE_TRANSFER_ESCROW, - expiredAt: Date, - token: Address = this.config.baseFare.contractAddress - ) { - try { - const data = encodeFunctionData({ - abi: ACP_ABI, - functionName: "createPayableMemo", - args: [ - jobId, - content, - token, - amountBaseUnit, - recipient, - feeAmountBaseUnit, - feeType, - type, - nextPhase, - Math.floor(expiredAt.getTime() / 1000), - ], - }); - - return await this.handleSendUserOperation(data); - } catch (error) { - throw new AcpError("Failed to create payable memo", error); - } - } - - async createMemo( - jobId: number, - content: string, - type: MemoType, - isSecured: boolean, - nextPhase: AcpJobPhases - ): Promise
{ - try { - const data = encodeFunctionData({ - abi: ACP_ABI, - functionName: "createMemo", - args: [jobId, content, type, isSecured, nextPhase], - }); - - return await this.handleSendUserOperation(data); - } catch (error) { - throw new AcpError("Failed to create memo", error); - } - } - - async getMemoId(hash: Address) { - const result = await this.sessionKeyClient.getUserOperationReceipt(hash); - - if (!result) { - throw new AcpError("Failed to get user operation receipt"); - } - - const contractLogs = result.logs.find( - (log: any) => - log.address.toLowerCase() === this.contractAddress.toLowerCase() - ) as any; - - if (!contractLogs) { - throw new AcpError("Failed to get contract logs"); - } - - const decoded = decodeEventLog({ - abi: ACP_ABI, - data: contractLogs.data, - topics: contractLogs.topics, - }); - - if (!decoded.args) { - throw new AcpError("Failed to decode event logs"); - } - - return parseInt((decoded.args as any).memoId); - } - - async signMemo(memoId: number, isApproved: boolean, reason?: string) { - try { - const data = encodeFunctionData({ - abi: ACP_ABI, - functionName: "signMemo", - args: [memoId, isApproved, reason], - }); - - return await this.handleSendUserOperation(data); - } catch (error) { - throw new AcpError("Failed to sign memo", error); - } - } - - async setBudget(jobId: number, budgetBaseUnit: bigint) { - try { - const data = encodeFunctionData({ - abi: ACP_ABI, - functionName: "setBudget", - args: [jobId, budgetBaseUnit], - }); - - return await this.handleSendUserOperation(data); - } catch (error) { - throw new AcpError("Failed to set budget", error); - } - } - - async setBudgetWithPaymentToken( - jobId: number, - budgetBaseUnit: bigint, - paymentTokenAddress: Address = this.config.baseFare.contractAddress - ) { - try { - const data = encodeFunctionData({ - abi: ACP_ABI, - functionName: "setBudgetWithPaymentToken", - args: [jobId, budgetBaseUnit, paymentTokenAddress], - }); - - return await this.handleSendUserOperation(data); - } catch (error) { - throw new AcpError("Failed to set budget", error); - } - } - - async wrapEth(amountBaseUnit: bigint) { - try { - const data = encodeFunctionData({ - abi: WETH_ABI, - functionName: "deposit", - }); - - return await this.handleSendUserOperation( - data, - wethFare.contractAddress, - amountBaseUnit - ); - } catch (error) { - throw new AcpError("Failed to wrap eth", error); - } - } -} - -export default AcpContractClient; diff --git a/src/acpFare.ts b/src/acpFare.ts index 10568ca..60e971b 100644 --- a/src/acpFare.ts +++ b/src/acpFare.ts @@ -1,5 +1,13 @@ -import { Address, ethAddress, parseUnits } from "viem"; +import { + Address, + createPublicClient, + erc20Abi, + ethAddress, + http, + parseUnits, +} from "viem"; import AcpError from "./acpError"; +import { AcpContractConfig, baseAcpConfig } from "./configs/acpConfigs"; class Fare { constructor(public contractAddress: Address, public decimals: number) {} @@ -7,47 +15,83 @@ class Fare { formatAmount(amount: number) { return parseUnits(amount.toString(), this.decimals); } -} -interface IFareAmount { - amount: bigint; - fare: Fare; - add(other: IFareAmount): IFareAmount; + static async fromContractAddress( + contractAddress: Address, + config: AcpContractConfig = baseAcpConfig + ) { + if (contractAddress === config.baseFare.contractAddress) { + return config.baseFare; + } + + const publicClient = createPublicClient({ + chain: config.chain, + transport: http(config.rpcEndpoint), + }); + + const decimals = await publicClient.readContract({ + address: contractAddress, + abi: erc20Abi, + functionName: "decimals", + }); + + return new Fare(contractAddress, decimals as number); + } } -class FareAmount implements IFareAmount { +abstract class FareAmountBase { amount: bigint; fare: Fare; - constructor(fareAmount: number, fare: Fare) { - this.amount = fare.formatAmount( - this.truncateTo6Decimals(fareAmount.toString()) - ); + constructor(amount: bigint, fare: Fare) { + this.amount = amount; this.fare = fare; } - truncateTo6Decimals(input: string): number { - const [intPart, decPart = ""] = input.split("."); + abstract add(other: FareAmountBase): FareAmountBase; - if (decPart === "") { - return parseFloat(intPart); + static async fromContractAddress( + amount: number | bigint, + contractAddress: Address, + config: AcpContractConfig = baseAcpConfig + ): Promise { + const fare = await Fare.fromContractAddress(contractAddress, config); + + if (typeof amount === "number") { + return new FareAmount(amount, fare); } - const truncated = decPart.slice(0, 6).padEnd(6, "0"); + return new FareBigInt(amount, fare); + } +} + +class FareAmount extends FareAmountBase { + constructor(fareAmount: number, fare: Fare) { + const truncateTo6Decimals = (input: string): number => { + const [intPart, decPart = ""] = input.split("."); + + if (decPart === "") { + return parseFloat(intPart); + } + + const truncated = decPart.slice(0, 6).padEnd(6, "0"); + + return parseFloat(`${intPart}.${truncated}`); + }; - return parseFloat(`${intPart}.${truncated}`); + super(fare.formatAmount(truncateTo6Decimals(fareAmount.toString())), fare); } - add(other: IFareAmount) { + add(other: FareAmountBase) { if (this.fare.contractAddress !== other.fare.contractAddress) { - throw new AcpError("Token addresses do not match"); + throw new Error("Token addresses do not match"); } - return new FareAmount(Number(this.amount + other.amount), this.fare); + return new FareBigInt(this.amount + other.amount, this.fare); } } -class FareBigInt implements IFareAmount { +class FareBigInt implements FareAmountBase { amount: bigint; fare: Fare; @@ -56,7 +100,7 @@ class FareBigInt implements IFareAmount { this.fare = fare; } - add(other: IFareAmount): IFareAmount { + add(other: FareAmountBase): FareAmountBase { if (this.fare.contractAddress !== other.fare.contractAddress) { throw new AcpError("Token addresses do not match"); } @@ -68,4 +112,4 @@ class FareBigInt implements IFareAmount { const wethFare = new Fare("0x4200000000000000000000000000000000000006", 18); const ethFare = new Fare(ethAddress, 18); -export { Fare, IFareAmount, FareAmount, FareBigInt, wethFare, ethFare }; +export { Fare, FareAmountBase, FareAmount, FareBigInt, wethFare, ethFare }; diff --git a/src/acpJob.ts b/src/acpJob.ts index 0d53ee4..6e9f6bb 100644 --- a/src/acpJob.ts +++ b/src/acpJob.ts @@ -1,25 +1,30 @@ import { Address } from "viem"; import AcpClient from "./acpClient"; -import { AcpJobPhases, FeeType, MemoType } from "./acpContractClient"; +import { + AcpJobPhases, + FeeType, + MemoType, +} from "./contractClients/baseAcpContractClient"; import AcpMemo from "./acpMemo"; import { CloseJobAndWithdrawPayload, ClosePositionPayload, GenericPayload, + IDeliverable, OpenPositionPayload, PayloadType, PositionFulfilledPayload, - UnfulfilledPositionPayload, RequestClosePositionPayload, - IDeliverable, SwapTokenPayload, + UnfulfilledPositionPayload, } from "./interfaces"; import { tryParseJson } from "./utils"; -import { Fare, FareAmount, IFareAmount } from "./acpFare"; +import { Fare, FareAmount, FareAmountBase } from "./acpFare"; import AcpError from "./acpError"; class AcpJob { - private baseFare: Fare; + public name: string | undefined; + public requirement: Record | string | undefined; constructor( private acpClient: AcpClient, @@ -31,50 +36,48 @@ class AcpJob { public priceTokenAddress: Address, public memos: AcpMemo[], public phase: AcpJobPhases, - public context: Record + public context: Record, + public contractAddress: Address ) { - this.baseFare = acpClient.acpContractClient.config.baseFare; - } - - public get serviceRequirement(): Record | string | undefined { const content = this.memos.find( (m) => m.nextPhase === AcpJobPhases.NEGOTIATION )?.content; if (!content) { - return undefined; + return; } const contentObj = tryParseJson<{ + name: string; + requirement: Record | string; serviceName: string; serviceRequirement: Record; }>(content); if (!contentObj) { - return content; + return; } - if (contentObj.serviceRequirement) { - return contentObj.serviceRequirement; + if (contentObj.serviceRequirement || contentObj.requirement) { + this.requirement = + contentObj.requirement || contentObj.serviceRequirement; } - return contentObj; + if (contentObj.serviceName || contentObj.name) { + this.name = contentObj.name || contentObj.serviceName; + } } - public get serviceName() { - const content = this.memos.find( - (m) => m.nextPhase === AcpJobPhases.NEGOTIATION - )?.content; - - if (!content) { - return undefined; - } + public get acpContractClient() { + return this.acpClient.contractClientByAddress(this.contractAddress); + } - const contentObj = tryParseJson<{ - serviceName: string; - }>(content); + public get config() { + return this.acpContractClient.config; + } - return contentObj?.serviceName; + public get baseFare() { + return this.acpContractClient.config.baseFare; } public get deliverable() { @@ -93,11 +96,56 @@ class AcpJob { public get evaluatorAgent() { return this.acpClient.getAgent(this.evaluatorAddress); } + + public get account() { + return this.acpClient.getAccountByJobId(this.id, this.acpContractClient); + } + public get latestMemo(): AcpMemo | undefined { return this.memos[this.memos.length - 1]; } - async pay(amount: number, reason?: string) { + async createRequirement(content: string) { + return await this.acpContractClient.createMemo( + this.id, + content, + MemoType.MESSAGE, + true, + AcpJobPhases.TRANSACTION + ); + } + + async createPayableRequirement( + content: string, + type: MemoType.PAYABLE_REQUEST | MemoType.PAYABLE_TRANSFER_ESCROW, + amount: FareAmountBase, + recipient: Address, + expiredAt: Date = new Date(Date.now() + 1000 * 60 * 5) // 5 minutes + ) { + if (type === MemoType.PAYABLE_TRANSFER_ESCROW) { + await this.acpContractClient.approveAllowance( + amount.amount, + amount.fare.contractAddress + ); + } + + const feeAmount = new FareAmount(0, this.acpContractClient.config.baseFare); + + return await this.acpContractClient.createPayableMemo( + this.id, + content, + amount.amount, + recipient, + feeAmount.amount, + FeeType.NO_FEE, + AcpJobPhases.TRANSACTION, + type, + expiredAt, + amount.fare.contractAddress + ); + } + + async payAndAcceptRequirement(reason?: string) { const memo = this.memos.find( (m) => m.nextPhase === AcpJobPhases.TRANSACTION ); @@ -106,29 +154,90 @@ class AcpJob { throw new AcpError("No transaction memo found"); } - return await this.acpClient.payJob( + const baseFareAmount = new FareAmount(this.price, this.baseFare); + const transferAmount = memo.payableDetails + ? await FareAmountBase.fromContractAddress( + memo.payableDetails.amount, + memo.payableDetails.token, + this.config + ) + : new FareAmount(0, this.baseFare); + + const totalAmount = + baseFareAmount.fare.contractAddress === + transferAmount.fare.contractAddress + ? baseFareAmount.add(transferAmount) + : baseFareAmount; + + await this.acpContractClient.approveAllowance( + totalAmount.amount, + this.baseFare.contractAddress + ); + + if ( + baseFareAmount.fare.contractAddress !== + transferAmount.fare.contractAddress + ) { + await this.acpContractClient.approveAllowance( + transferAmount.amount, + transferAmount.fare.contractAddress + ); + } + + await memo.sign(true, reason); + + return await this.acpContractClient.createMemo( this.id, - this.baseFare.formatAmount(amount), - memo.id, - reason + `Payment made. ${reason ?? ""}`.trim(), + MemoType.MESSAGE, + true, + AcpJobPhases.EVALUATION ); } - async respond( - accept: boolean, - payload?: GenericPayload, - reason?: string - ) { + async respond(accept: boolean, reason?: string) { + if (accept) { + return await this.accept(reason); + } + + return await this.reject(reason); + } + + async accept(reason?: string) { if (this.latestMemo?.nextPhase !== AcpJobPhases.NEGOTIATION) { throw new AcpError("No negotiation memo found"); } - return await this.acpClient.respondJob( + const memo = this.latestMemo; + + await memo.sign(true, reason); + + return await this.acpContractClient.createMemo( this.id, - this.latestMemo.id, - accept, - payload ? JSON.stringify(payload) : undefined, - reason + `Job ${this.id} accepted. ${reason ?? ""}`, + MemoType.MESSAGE, + true, + AcpJobPhases.TRANSACTION + ); + } + + async reject(reason?: string) { + const memoContent = `Job ${this.id} rejected. ${reason || ""}`; + if (this.phase === AcpJobPhases.REQUEST) { + if (this.latestMemo?.nextPhase !== AcpJobPhases.NEGOTIATION) { + throw new AcpError("No negotiation memo found"); + } + const memo = this.latestMemo; + + return await this.acpContractClient.signMemo(memo.id, false, memoContent); + } + + return await this.acpContractClient.createMemo( + this.id, + memoContent, + MemoType.MESSAGE, + true, + AcpJobPhases.REJECTED ); } @@ -137,7 +246,43 @@ class AcpJob { throw new AcpError("No transaction memo found"); } - return await this.acpClient.deliverJob(this.id, deliverable); + return await this.acpContractClient.createMemo( + this.id, + JSON.stringify(deliverable), + MemoType.MESSAGE, + true, + AcpJobPhases.COMPLETED + ); + } + + async deliverPayable( + deliverable: IDeliverable, + amount: FareAmountBase, + expiredAt: Date = new Date(Date.now() + 1000 * 60 * 5) // 5 minutes + ) { + if (this.latestMemo?.nextPhase !== AcpJobPhases.EVALUATION) { + throw new AcpError("No transaction memo found"); + } + + await this.acpContractClient.approveAllowance( + amount.amount, + amount.fare.contractAddress + ); + + const feeAmount = new FareAmount(0, this.acpContractClient.config.baseFare); + + return await this.acpContractClient.createPayableMemo( + this.id, + JSON.stringify(deliverable), + amount.amount, + this.clientAddress, + feeAmount.amount, + FeeType.NO_FEE, + AcpJobPhases.COMPLETED, + MemoType.PAYABLE_TRANSFER, + expiredAt, + amount.fare.contractAddress + ); } async evaluate(accept: boolean, reason?: string) { @@ -145,13 +290,69 @@ class AcpJob { throw new AcpError("No evaluation memo found"); } - return await this.acpClient.acpContractClient.signMemo( - this.latestMemo.id, - accept, + const memo = this.latestMemo; + + await memo.sign(accept, reason); + } + + async pay(reason?: string) { + const memo = this.memos.find( + (m) => m.nextPhase === AcpJobPhases.TRANSACTION + ); + + if (!memo) { + throw new AcpError("No transaction memo found"); + } + + return await this.acpClient.payJob( + this.id, + this.baseFare.formatAmount(this.price), + memo.id, reason ); } + async createNotification(content: string) { + return await this.acpContractClient.createMemo( + this.id, + content, + MemoType.NOTIFICATION, + true, + AcpJobPhases.COMPLETED + ); + } + + async createPayableNotification( + content: string, + amount: FareAmountBase, + expiredAt: Date = new Date(Date.now() + 1000 * 60 * 5) // 5 minutes + ) { + await this.acpContractClient.approveAllowance( + amount.amount, + amount.fare.contractAddress + ); + + const feeAmount = new FareAmount(0, this.acpContractClient.config.baseFare); + + return await this.acpContractClient.createPayableMemo( + this.id, + content, + amount.amount, + this.clientAddress, + feeAmount.amount, + FeeType.NO_FEE, + AcpJobPhases.COMPLETED, + MemoType.PAYABLE_NOTIFICATION, + expiredAt, + amount.fare.contractAddress + ); + } + + // to be deprecated + + /** + * @deprecated The method should not be used + */ async openPosition( payload: OpenPositionPayload[], feeAmount: number, @@ -179,6 +380,9 @@ class AcpJob { ); } + /** + * @deprecated The method should not be used + */ async swapToken( payload: SwapTokenPayload, decimals: number, @@ -203,6 +407,9 @@ class AcpJob { ); } + /** + * @deprecated The method should not be used + */ async responseSwapToken(memoId: number, accept: boolean, reason: string) { const memo = this.memos.find((m) => m.id === memoId); @@ -224,9 +431,12 @@ class AcpJob { return await memo.sign(accept, reason); } + /** + * @deprecated The method should not be used + */ async transferFunds( payload: GenericPayload, - fareAmount: IFareAmount, + fareAmount: FareAmountBase, walletAddress?: Address, expiredAt: Date = new Date(Date.now() + 1000 * 60 * 30) ) { @@ -242,6 +452,9 @@ class AcpJob { ); } + /** + * @deprecated The method should not be used + */ async responseOpenPosition(memoId: number, accept: boolean, reason: string) { const memo = this.memos.find((m) => m.id === memoId); @@ -263,6 +476,9 @@ class AcpJob { return await this.acpClient.responseFundsTransfer(memo.id, accept, reason); } + /** + * @deprecated The method should not be used + */ async closePartialPosition( payload: ClosePositionPayload, expireAt: Date = new Date(Date.now() + 1000 * 60 * 60 * 24) // 24 hours @@ -282,6 +498,9 @@ class AcpJob { ); } + /** + * @deprecated The method should not be used + */ async responseClosePartialPosition( memoId: number, accept: boolean, @@ -312,6 +531,9 @@ class AcpJob { ); } + /** + * @deprecated The method should not be used + */ async requestClosePosition(payload: RequestClosePositionPayload) { return await this.acpClient.sendMessage( this.id, @@ -323,6 +545,9 @@ class AcpJob { ); } + /** + * @deprecated The method should not be used + */ async responseRequestClosePosition( memoId: number, accept: boolean, @@ -366,6 +591,9 @@ class AcpJob { } } + /** + * @deprecated The method should not be used + */ async confirmClosePosition(memoId: number, accept: boolean, reason?: string) { const memo = this.memos.find((m) => m.id === memoId); @@ -387,6 +615,9 @@ class AcpJob { await memo.sign(accept, reason); } + /** + * @deprecated The method should not be used + */ async positionFulfilled( payload: PositionFulfilledPayload, expiredAt: Date = new Date(Date.now() + 1000 * 60 * 60 * 24) // 24 hours @@ -406,6 +637,9 @@ class AcpJob { ); } + /** + * @deprecated The method should not be used + */ async unfulfilledPosition( payload: UnfulfilledPositionPayload, expiredAt: Date = new Date(Date.now() + 1000 * 60 * 60 * 24) // 24 hours @@ -425,6 +659,9 @@ class AcpJob { ); } + /** + * @deprecated The method should not be used + */ async responseUnfulfilledPosition( memoId: number, accept: boolean, @@ -450,6 +687,9 @@ class AcpJob { return await this.acpClient.responseFundsTransfer(memo.id, accept, reason); } + /** + * @deprecated The method should not be used + */ async responsePositionFulfilled( memoId: number, accept: boolean, @@ -475,6 +715,9 @@ class AcpJob { return await this.acpClient.responseFundsTransfer(memo.id, accept, reason); } + /** + * @deprecated The method should not be used + */ async closeJob(message: string = "Close job and withdraw all") { return await this.acpClient.sendMessage( this.id, @@ -488,6 +731,9 @@ class AcpJob { ); } + /** + * @deprecated The method should not be used + */ async responseCloseJob( memoId: number, accept: boolean, @@ -549,6 +795,9 @@ class AcpJob { ); } + /** + * @deprecated The method should not be used + */ async confirmJobClosure(memoId: number, accept: boolean, reason?: string) { const memo = this.memos.find((m) => m.id === memoId); diff --git a/src/acpJobOffering.ts b/src/acpJobOffering.ts index 303c264..8a013ed 100644 --- a/src/acpJobOffering.ts +++ b/src/acpJobOffering.ts @@ -1,18 +1,24 @@ -import { Address } from "viem"; +import { Address, zeroAddress } from "viem"; import AcpClient from "./acpClient"; import Ajv from "ajv"; import { FareAmount } from "./acpFare"; import AcpError from "./acpError"; +import BaseAcpContractClient, { + AcpJobPhases, + MemoType, +} from "./contractClients/baseAcpContractClient"; +import { baseAcpConfig, baseSepoliaAcpConfig } from "./configs/acpConfigs"; class AcpJobOffering { private ajv: Ajv; constructor( private readonly acpClient: AcpClient, + private readonly acpContractClient: BaseAcpContractClient, public providerAddress: Address, public name: string, public price: number, - public requirementSchema?: Object + public requirement?: Object | string ) { this.ajv = new Ajv({ allErrors: true }); } @@ -22,8 +28,8 @@ class AcpJobOffering { evaluatorAddress?: Address, expiredAt: Date = new Date(Date.now() + 1000 * 60 * 60 * 24) // default: 1 day ) { - if (this.requirementSchema) { - const validator = this.ajv.compile(this.requirementSchema); + if (this.requirement && typeof this.requirement === "object") { + const validator = this.ajv.compile(this.requirement); const valid = validator(serviceRequirement); if (!valid) { @@ -31,32 +37,53 @@ class AcpJobOffering { } } - let finalServiceRequirement: Record = { - serviceName: this.name, + const finalServiceRequirement: Record = { + name: this.name, + requirement: serviceRequirement, }; - if (typeof serviceRequirement === "string") { - finalServiceRequirement = { - ...finalServiceRequirement, - message: serviceRequirement, - }; - } else { - finalServiceRequirement = { - ...finalServiceRequirement, - serviceRequirement: serviceRequirement, - }; - } + const fareAmount = new FareAmount( + this.price, + this.acpContractClient.config.baseFare + ); - return await this.acpClient.initiateJob( + const account = await this.acpClient.getByClientAndProvider( + this.acpContractClient.walletAddress, this.providerAddress, - finalServiceRequirement, - new FareAmount( - this.price, - this.acpClient.acpContractClient.config.baseFare - ), - evaluatorAddress, - expiredAt + this.acpContractClient + ); + + const { jobId, txHash } = + [ + baseSepoliaAcpConfig.contractAddress, + baseAcpConfig.contractAddress, + ].includes(this.acpContractClient.config.contractAddress) || !account + ? await this.acpContractClient.createJob( + this.providerAddress, + evaluatorAddress || this.acpContractClient.walletAddress, + expiredAt, + fareAmount.fare.contractAddress, + fareAmount.amount, + "" + ) + : await this.acpContractClient.createJobWithAccount( + account.id, + this.providerAddress, + evaluatorAddress || zeroAddress, + fareAmount.amount, + fareAmount.fare.contractAddress, + expiredAt + ); + + await this.acpContractClient.createMemo( + jobId, + JSON.stringify(finalServiceRequirement), + MemoType.MESSAGE, + true, + AcpJobPhases.NEGOTIATION ); + + return jobId; } } diff --git a/src/acpMemo.ts b/src/acpMemo.ts index f5643ef..3d78cd7 100644 --- a/src/acpMemo.ts +++ b/src/acpMemo.ts @@ -1,6 +1,8 @@ import { Address } from "viem"; -import AcpClient from "./acpClient"; -import { AcpJobPhases, MemoType } from "./acpContractClient"; +import BaseAcpContractClient, { + AcpJobPhases, + MemoType, +} from "./contractClients/baseAcpContractClient"; import { AcpMemoStatus, GenericPayload, @@ -13,7 +15,7 @@ class AcpMemo { structuredContent: GenericPayload | undefined; constructor( - private acpClient: AcpClient, + private contractClient: BaseAcpContractClient, public id: number, public type: MemoType, public content: string, @@ -42,7 +44,7 @@ class AcpMemo { } async create(jobId: number, isSecured: boolean = true) { - return await this.acpClient.acpContractClient.createMemo( + return await this.contractClient.createMemo( jobId, this.content, this.type, @@ -52,11 +54,7 @@ class AcpMemo { } async sign(approved: boolean, reason?: string) { - return await this.acpClient.acpContractClient.signMemo( - this.id, - approved, - reason - ); + return await this.contractClient.signMemo(this.id, approved, reason); } } diff --git a/src/configs/acpConfigs.ts b/src/configs/acpConfigs.ts new file mode 100644 index 0000000..0e4ae83 --- /dev/null +++ b/src/configs/acpConfigs.ts @@ -0,0 +1,61 @@ +import { Address } from "@aa-sdk/core"; +import { baseSepolia, base } from "@account-kit/infra"; +import { Fare } from "../acpFare"; +import ACP_ABI from "../abis/acpAbi"; +import ACP_V2_ABI from "../abis/acpAbiV2"; + +class AcpContractConfig { + constructor( + public chain: typeof baseSepolia | typeof base, + public contractAddress: Address, + public baseFare: Fare, + public alchemyRpcUrl: string, + public acpUrl: string, + public abi: typeof ACP_ABI | typeof ACP_V2_ABI, + public rpcEndpoint?: string + ) {} +} + +const baseSepoliaAcpConfig = new AcpContractConfig( + baseSepolia, + "0x8Db6B1c839Fc8f6bd35777E194677B67b4D51928", + new Fare("0x036CbD53842c5426634e7929541eC2318f3dCF7e", 6), + "https://alchemy-proxy.virtuals.io/api/proxy/rpc", + "https://acpx.virtuals.gg", + ACP_ABI +); + +const baseSepoliaAcpConfigV2 = new AcpContractConfig( + baseSepolia, + "0xdf54E6Ed6cD1d0632d973ADECf96597b7e87893c", + new Fare("0x036CbD53842c5426634e7929541eC2318f3dCF7e", 6), + "https://alchemy-proxy.virtuals.io/api/proxy/rpc", + "https://acpx.virtuals.gg", + ACP_V2_ABI +); + +const baseAcpConfig = new AcpContractConfig( + base, + "0x6a1FE26D54ab0d3E1e3168f2e0c0cDa5cC0A0A4A", + new Fare("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", 6), + "https://alchemy-proxy-prod.virtuals.io/api/proxy/rpc", + "https://acpx.virtuals.io", + ACP_ABI +); + +const baseAcpConfigV2 = new AcpContractConfig( + base, + "0xa6C9BA866992cfD7fd6460ba912bfa405adA9df0", + new Fare("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", 6), + "https://alchemy-proxy-prod.virtuals.io/api/proxy/rpc", + "https://acpx.virtuals.io", + ACP_V2_ABI +); + +export { + AcpContractConfig, + baseSepoliaAcpConfigV2, + baseSepoliaAcpConfig, + baseAcpConfig, + baseAcpConfigV2, +}; diff --git a/src/contractClients/acpContractClient.ts b/src/contractClients/acpContractClient.ts new file mode 100644 index 0000000..8c32c4b --- /dev/null +++ b/src/contractClients/acpContractClient.ts @@ -0,0 +1,275 @@ +import { Address, LocalAccountSigner, SmartAccountSigner } from "@aa-sdk/core"; +import { alchemy } from "@account-kit/infra"; +import { + ModularAccountV2Client, + createModularAccountV2Client, +} from "@account-kit/smart-contracts"; +import { decodeEventLog, encodeFunctionData, fromHex } from "viem"; +import { AcpContractConfig, baseAcpConfig } from "../configs/acpConfigs"; +import AcpError from "../acpError"; +import BaseAcpContractClient, { + AcpJobPhases, + FeeType, + MemoType, +} from "./baseAcpContractClient"; + +class AcpContractClient extends BaseAcpContractClient { + protected MAX_RETRIES = 3; + protected PRIORITY_FEE_MULTIPLIER = 2; + protected MAX_FEE_PER_GAS = 20000000; + protected MAX_PRIORITY_FEE_PER_GAS = 21000000; + + private _sessionKeyClient: ModularAccountV2Client | undefined; + + constructor( + agentWalletAddress: Address, + config: AcpContractConfig = baseAcpConfig + ) { + super(agentWalletAddress, config); + } + + static async build( + walletPrivateKey: Address, + sessionEntityKeyId: number, + agentWalletAddress: Address, + config: AcpContractConfig = baseAcpConfig + ) { + const acpContractClient = new AcpContractClient(agentWalletAddress, config); + await acpContractClient.init(walletPrivateKey, sessionEntityKeyId); + return acpContractClient; + } + + async init(privateKey: Address, sessionEntityKeyId: number) { + const sessionKeySigner: SmartAccountSigner = + LocalAccountSigner.privateKeyToAccountSigner(privateKey); + + this._sessionKeyClient = await createModularAccountV2Client({ + chain: this.chain, + transport: alchemy({ + rpcUrl: this.config.alchemyRpcUrl, + }), + signer: sessionKeySigner, + policyId: "186aaa4a-5f57-4156-83fb-e456365a8820", + accountAddress: this.agentWalletAddress, + signerEntity: { + entityId: sessionEntityKeyId, + isGlobalValidation: true, + }, + }); + } + + getRandomNonce(bits = 152) { + const bytes = bits / 8; + const array = new Uint8Array(bytes); + crypto.getRandomValues(array); + + let hex = Array.from(array, (b) => b.toString(16).padStart(2, "0")).join( + "" + ); + return BigInt("0x" + hex); + } + + get sessionKeyClient() { + if (!this._sessionKeyClient) { + throw new AcpError("Session key client not initialized"); + } + + return this._sessionKeyClient; + } + + private async calculateGasFees() { + const finalMaxFeePerGas = + BigInt(this.MAX_FEE_PER_GAS) + + BigInt(this.MAX_PRIORITY_FEE_PER_GAS) * + BigInt(Math.max(0, this.PRIORITY_FEE_MULTIPLIER - 1)); + + return finalMaxFeePerGas; + } + + async handleOperation( + data: `0x${string}`, + contractAddress: Address = this.contractAddress, + value?: bigint + ) { + const payload: any = { + uo: { + target: contractAddress, + data: data, + value: value, + }, + overrides: { + nonceKey: this.getRandomNonce(), + }, + }; + + let retries = this.MAX_RETRIES; + let finalError: unknown; + + while (retries > 0) { + try { + if (this.MAX_RETRIES > retries) { + const gasFees = await this.calculateGasFees(); + + payload["overrides"] = { + maxFeePerGas: `0x${gasFees.toString(16)}`, + }; + } + + const { hash } = await this.sessionKeyClient.sendUserOperation(payload); + + await this.sessionKeyClient.waitForUserOperationTransaction({ + hash, + }); + + return hash; + } catch (error) { + retries -= 1; + if (retries === 0) { + finalError = error; + break; + } + + await new Promise((resolve) => setTimeout(resolve, 2000 * retries)); + } + } + + throw new AcpError(`Failed to send user operation`, finalError); + } + + async getJobId( + hash: Address, + clientAddress: Address, + providerAddress: Address + ) { + const result = await this.sessionKeyClient.getUserOperationReceipt(hash); + + if (!result) { + throw new AcpError("Failed to get user operation receipt"); + } + + const contractLogs = result.logs + .filter((log: any) => { + return log.address.toLowerCase() === this.contractAddress.toLowerCase(); + }) + .map( + (log: any) => + decodeEventLog({ + abi: this.abi, + data: log.data, + topics: log.topics, + }) as { + eventName: string; + args: any; + } + ); + + const createdJobEvent = contractLogs.find( + (log) => + log.eventName === "JobCreated" && + log.args.client.toLowerCase() === clientAddress.toLowerCase() && + log.args.provider.toLowerCase() === providerAddress.toLowerCase() + ); + + if (!createdJobEvent) { + throw new AcpError("Failed to find created job event"); + } + + return Number(createdJobEvent.args.jobId); + } + + async createJob( + providerAddress: Address, + evaluatorAddress: Address, + expireAt: Date, + paymentTokenAddress: Address, + budgetBaseUnit: bigint, + metadata: string + ): Promise<{ txHash: string; jobId: number }> { + try { + const data = encodeFunctionData({ + abi: this.abi, + functionName: "createJob", + args: [ + providerAddress, + evaluatorAddress, + Math.floor(expireAt.getTime() / 1000), + ], + }); + + const hash = await this.handleOperation(data, this.contractAddress); + + const jobId = await this.getJobId( + hash, + this.agentWalletAddress, + providerAddress + ); + + await this.setBudgetWithPaymentToken( + jobId, + budgetBaseUnit, + paymentTokenAddress + ); + + return { txHash: hash, jobId: jobId }; + } catch (error) { + throw new AcpError("Failed to create job", error); + } + } + + async createPayableMemo( + jobId: number, + content: string, + amountBaseUnit: bigint, + recipient: Address, + feeAmountBaseUnit: bigint, + feeType: FeeType, + nextPhase: AcpJobPhases, + type: MemoType.PAYABLE_REQUEST | MemoType.PAYABLE_TRANSFER_ESCROW, + expiredAt: Date, + token: Address = this.config.baseFare.contractAddress, + secured: boolean = true + ) { + try { + const data = encodeFunctionData({ + abi: this.abi, + functionName: "createPayableMemo", + args: [ + jobId, + content, + token, + amountBaseUnit, + recipient, + feeAmountBaseUnit, + feeType, + type, + nextPhase, + Math.floor(expiredAt.getTime() / 1000), + ], + }); + + return await this.handleOperation(data, this.contractAddress); + } catch (error) { + throw new AcpError("Failed to create payable memo", error); + } + } + + async createJobWithAccount( + accountId: number, + providerAddress: Address, + evaluatorAddress: Address, + budgetBaseUnit: bigint, + paymentTokenAddress: Address, + expiredAt: Date + ): Promise<{ txHash: string; jobId: number }> { + throw new AcpError("Not Supported"); + } + + async updateAccountMetadata( + accountId: number, + metadata: string + ): Promise
{ + throw new AcpError("Not Supported"); + } +} + +export default AcpContractClient; diff --git a/src/contractClients/acpContractClientV2.ts b/src/contractClients/acpContractClientV2.ts new file mode 100644 index 0000000..3b40d27 --- /dev/null +++ b/src/contractClients/acpContractClientV2.ts @@ -0,0 +1,223 @@ +import { Address, LocalAccountSigner, SmartAccountSigner } from "@aa-sdk/core"; +import { alchemy } from "@account-kit/infra"; +import { + ModularAccountV2Client, + createModularAccountV2Client, +} from "@account-kit/smart-contracts"; +import { createPublicClient, decodeEventLog, fromHex, http } from "viem"; +import { AcpContractConfig, baseAcpConfigV2 } from "../configs/acpConfigs"; +import AcpError from "../acpError"; +import BaseAcpContractClient from "./baseAcpContractClient"; +import JOB_MANAGER_ABI from "../abis/jobManagerAbi"; + +class AcpContractClientV2 extends BaseAcpContractClient { + private MAX_RETRIES = 3; + private PRIORITY_FEE_MULTIPLIER = 2; + private MAX_FEE_PER_GAS = 20000000; + private MAX_PRIORITY_FEE_PER_GAS = 21000000; + + private _sessionKeyClient: ModularAccountV2Client | undefined; + + constructor( + private jobManagerAddress: Address, + private memoManagerAddress: Address, + private accountManagerAddress: Address, + agentWalletAddress: Address, + config: AcpContractConfig = baseAcpConfigV2 + ) { + super(agentWalletAddress, config); + } + + static async build( + walletPrivateKey: Address, + sessionEntityKeyId: number, + agentWalletAddress: Address, + config: AcpContractConfig = baseAcpConfigV2 + ) { + const publicClient = createPublicClient({ + chain: config.chain, + transport: http(config.rpcEndpoint), + }); + + const [jobManagerAddress, memoManagerAddress, accountManagerAddress] = + await publicClient.multicall({ + contracts: [ + { + address: config.contractAddress, + abi: config.abi, + functionName: "jobManager", + }, + { + address: config.contractAddress, + abi: config.abi, + functionName: "memoManager", + }, + { + address: config.contractAddress, + abi: config.abi, + functionName: "accountManager", + }, + ], + }); + + if (!jobManagerAddress || !memoManagerAddress || !accountManagerAddress) { + throw new AcpError( + "Failed to get job manager, memo manager, or account manager address" + ); + } + + const acpContractClient = new AcpContractClientV2( + jobManagerAddress.result as Address, + memoManagerAddress.result as Address, + accountManagerAddress.result as Address, + agentWalletAddress, + config + ); + + await acpContractClient.init(walletPrivateKey, sessionEntityKeyId); + + return acpContractClient; + } + + async init(privateKey: Address, sessionEntityKeyId: number) { + const sessionKeySigner: SmartAccountSigner = + LocalAccountSigner.privateKeyToAccountSigner(privateKey); + + this._sessionKeyClient = await createModularAccountV2Client({ + chain: this.chain, + transport: alchemy({ + rpcUrl: this.config.alchemyRpcUrl, + }), + signer: sessionKeySigner, + policyId: "186aaa4a-5f57-4156-83fb-e456365a8820", + accountAddress: this.agentWalletAddress, + signerEntity: { + entityId: sessionEntityKeyId, + isGlobalValidation: true, + }, + }); + } + + getRandomNonce(bits = 152) { + const bytes = bits / 8; + const array = new Uint8Array(bytes); + crypto.getRandomValues(array); + + let hex = Array.from(array, (b) => b.toString(16).padStart(2, "0")).join( + "" + ); + return BigInt("0x" + hex); + } + + get sessionKeyClient() { + if (!this._sessionKeyClient) { + throw new AcpError("Session key client not initialized"); + } + + return this._sessionKeyClient; + } + + private async calculateGasFees() { + const finalMaxFeePerGas = + BigInt(this.MAX_FEE_PER_GAS) + + BigInt(this.MAX_PRIORITY_FEE_PER_GAS) * + BigInt(Math.max(0, this.PRIORITY_FEE_MULTIPLIER - 1)); + + return finalMaxFeePerGas; + } + + async handleOperation( + data: `0x${string}`, + contractAddress: Address = this.contractAddress, + value?: bigint + ) { + const payload: any = { + uo: { + target: contractAddress, + data: data, + value: value, + }, + overrides: { + nonceKey: this.getRandomNonce(), + }, + }; + + let retries = this.MAX_RETRIES; + let finalError: unknown; + + while (retries > 0) { + try { + if (this.MAX_RETRIES > retries) { + const gasFees = await this.calculateGasFees(); + + payload["overrides"] = { + maxFeePerGas: `0x${gasFees.toString(16)}`, + }; + } + + const { hash } = await this.sessionKeyClient.sendUserOperation(payload); + + await this.sessionKeyClient.waitForUserOperationTransaction({ + hash, + }); + + return hash; + } catch (error) { + retries -= 1; + if (retries === 0) { + finalError = error; + break; + } + + await new Promise((resolve) => setTimeout(resolve, 2000 * retries)); + } + } + + throw new AcpError(`Failed to send user operation`, finalError); + } + + async getJobId( + hash: Address, + clientAddress: Address, + providerAddress: Address + ) { + const result = await this.sessionKeyClient.getUserOperationReceipt(hash); + + if (!result) { + throw new AcpError("Failed to get user operation receipt"); + } + + const contractLogs = result.logs + .filter((log: any) => { + return ( + log.address.toLowerCase() === this.jobManagerAddress.toLowerCase() + ); + }) + .map( + (log: any) => + decodeEventLog({ + abi: JOB_MANAGER_ABI, + data: log.data, + topics: log.topics, + }) as { + eventName: string; + args: any; + } + ); + + const createdJobEvent = contractLogs.find( + (log) => + log.eventName === "JobCreated" && + log.args.client.toLowerCase() === clientAddress.toLowerCase() && + log.args.provider.toLowerCase() === providerAddress.toLowerCase() + ); + + if (!createdJobEvent) { + throw new AcpError("Failed to find created job event"); + } + + return Number(createdJobEvent.args.jobId); + } +} + +export default AcpContractClientV2; diff --git a/src/contractClients/baseAcpContractClient.ts b/src/contractClients/baseAcpContractClient.ts new file mode 100644 index 0000000..dea9261 --- /dev/null +++ b/src/contractClients/baseAcpContractClient.ts @@ -0,0 +1,301 @@ +import { + AbiEvent, + Address, + Chain, + encodeFunctionData, + erc20Abi, + keccak256, + toEventSignature, + toHex, +} from "viem"; +import { AcpContractConfig, baseAcpConfig } from "../configs/acpConfigs"; +import ACP_V2_ABI from "../abis/acpAbiV2"; +import ACP_ABI from "../abis/acpAbi"; +import AcpError from "../acpError"; +import WETH_ABI from "../abis/wethAbi"; +import { wethFare } from "../acpFare"; + +export enum MemoType { + MESSAGE, // 0 - Text message + CONTEXT_URL, // 1 - URL for context + IMAGE_URL, // 2 - Image URL + VOICE_URL, // 3 - Voice/audio URL + OBJECT_URL, // 4 - Object/file URL + TXHASH, // 5 - Transaction hash reference + PAYABLE_REQUEST, // 6 - Payment request + PAYABLE_TRANSFER, // 7 - Direct payment transfer + PAYABLE_TRANSFER_ESCROW, // 8 - Escrowed payment transfer + NOTIFICATION, // 9 - Notification + PAYABLE_NOTIFICATION, // 10 - Payable notification +} + +export enum AcpJobPhases { + REQUEST = 0, + NEGOTIATION = 1, + TRANSACTION = 2, + EVALUATION = 3, + COMPLETED = 4, + REJECTED = 5, + EXPIRED = 6, +} + +export enum FeeType { + NO_FEE, + IMMEDIATE_FEE, + DEFERRED_FEE, +} + +abstract class BaseAcpContractClient { + public contractAddress: Address; + public chain: Chain; + public abi: typeof ACP_ABI | typeof ACP_V2_ABI; + public jobCreatedSignature: string; + + constructor( + public agentWalletAddress: Address, + public config: AcpContractConfig = baseAcpConfig + ) { + this.chain = config.chain; + this.abi = config.abi; + this.contractAddress = config.contractAddress; + + const jobCreated = ACP_ABI.find( + (abi) => abi.name === "JobCreated" + ) as AbiEvent; + const signature = toEventSignature(jobCreated); + this.jobCreatedSignature = keccak256(toHex(signature)); + } + + abstract handleOperation( + data: `0x${string}`, + contractAddress: Address, + value?: bigint + ): Promise
; + + abstract getJobId( + hash: Address, + clientAddress: Address, + providerAddress: Address + ): Promise; + + get walletAddress() { + return this.agentWalletAddress; + } + + async createJobWithAccount( + accountId: number, + providerAddress: Address, + evaluatorAddress: Address, + budgetBaseUnit: bigint, + paymentTokenAddress: Address, + expiredAt: Date + ): Promise<{ txHash: string; jobId: number }> { + try { + const data = encodeFunctionData({ + abi: this.abi, + functionName: "createJobWithAccount", + args: [ + accountId, + evaluatorAddress, + budgetBaseUnit, + paymentTokenAddress, + Math.floor(expiredAt.getTime() / 1000), + ], + }); + + const hash = await this.handleOperation(data, this.contractAddress); + + const jobId = await this.getJobId( + hash, + this.agentWalletAddress, + providerAddress + ); + + return { txHash: hash, jobId: jobId }; + } catch (error) { + throw new AcpError("Failed to create job with account", error); + } + } + + async createJob( + providerAddress: Address, + evaluatorAddress: Address, + expiredAt: Date, + paymentTokenAddress: Address, + budgetBaseUnit: bigint, + metadata: string + ): Promise<{ txHash: string; jobId: number }> { + try { + const data = encodeFunctionData({ + abi: this.abi, + functionName: "createJob", + args: [ + providerAddress, + evaluatorAddress, + Math.floor(expiredAt.getTime() / 1000), + paymentTokenAddress, + budgetBaseUnit, + metadata, + ], + }); + + const hash = await this.handleOperation(data, this.contractAddress); + + const jobId = await this.getJobId( + hash, + this.agentWalletAddress, + providerAddress + ); + + return { txHash: hash, jobId: jobId }; + } catch (error) { + throw new AcpError("Failed to create job", error); + } + } + + async approveAllowance( + amountBaseUnit: bigint, + paymentTokenAddress: Address = this.config.baseFare.contractAddress + ) { + try { + const data = encodeFunctionData({ + abi: erc20Abi, + functionName: "approve", + args: [this.contractAddress, amountBaseUnit], + }); + + return await this.handleOperation(data, paymentTokenAddress); + } catch (error) { + throw new AcpError("Failed to approve allowance", error); + } + } + + async createPayableMemo( + jobId: number, + content: string, + amountBaseUnit: bigint, + recipient: Address, + feeAmountBaseUnit: bigint, + feeType: FeeType, + nextPhase: AcpJobPhases, + type: + | MemoType.PAYABLE_REQUEST + | MemoType.PAYABLE_TRANSFER_ESCROW + | MemoType.PAYABLE_TRANSFER + | MemoType.PAYABLE_NOTIFICATION, + expiredAt: Date, + token: Address = this.config.baseFare.contractAddress, + secured: boolean = true + ) { + try { + const data = encodeFunctionData({ + abi: this.abi, + functionName: "createPayableMemo", + args: [ + jobId, + content, + token, + amountBaseUnit, + recipient, + feeAmountBaseUnit, + feeType, + type, + Math.floor(expiredAt.getTime() / 1000), + secured, + nextPhase, + ], + }); + + return await this.handleOperation(data, this.contractAddress); + } catch (error) { + throw new AcpError("Failed to create payable memo", error); + } + } + + async createMemo( + jobId: number, + content: string, + type: MemoType, + isSecured: boolean, + nextPhase: AcpJobPhases + ): Promise
{ + try { + const data = encodeFunctionData({ + abi: this.abi, + functionName: "createMemo", + args: [jobId, content, type, isSecured, nextPhase], + }); + + return await this.handleOperation(data, this.contractAddress); + } catch (error) { + throw new AcpError("Failed to create memo", error); + } + } + + async signMemo(memoId: number, isApproved: boolean, reason?: string) { + try { + const data = encodeFunctionData({ + abi: this.abi, + functionName: "signMemo", + args: [memoId, isApproved, reason], + }); + + const hash = await this.handleOperation(data, this.contractAddress); + + return hash; + } catch (error) { + throw new AcpError("Failed to sign memo", error); + } + } + + async setBudgetWithPaymentToken( + jobId: number, + budgetBaseUnit: bigint, + paymentTokenAddress: Address = this.config.baseFare.contractAddress + ) { + try { + const data = encodeFunctionData({ + abi: this.abi, + functionName: "setBudgetWithPaymentToken", + args: [jobId, budgetBaseUnit, paymentTokenAddress], + }); + + return await this.handleOperation(data, this.contractAddress); + } catch (error) { + throw new AcpError("Failed to set budget", error); + } + } + + async updateAccountMetadata(accountId: number, metadata: string) { + try { + const data = encodeFunctionData({ + abi: this.abi, + functionName: "updateAccountMetadata", + args: [accountId, metadata], + }); + + return await this.handleOperation(data, this.contractAddress); + } catch (error) { + throw new AcpError("Failed to update account metadata", error); + } + } + + async wrapEth(amountBaseUnit: bigint) { + try { + const data = encodeFunctionData({ + abi: WETH_ABI, + functionName: "deposit", + }); + + return await this.handleOperation( + data, + wethFare.contractAddress, + amountBaseUnit + ); + } catch (error) { + throw new AcpError("Failed to wrap eth", error); + } + } +} + +export default BaseAcpContractClient; diff --git a/src/index.ts b/src/index.ts index b77b67b..76443a2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,10 @@ -import ACP_ABI from "./acpAbi"; +import ACP_ABI from "./abis/acpAbi"; import AcpClient from "./acpClient"; -import AcpContractClient, { AcpJobPhases, MemoType } from "./acpContractClient"; +import AcpContractClient from "./contractClients/acpContractClient"; +import BaseAcpContractClient, { + AcpJobPhases, + MemoType, +} from "./contractClients/baseAcpContractClient"; import AcpJob from "./acpJob"; import AcpMemo from "./acpMemo"; import { @@ -21,16 +25,21 @@ import { import { AcpContractConfig, baseAcpConfig, + baseAcpConfigV2, baseSepoliaAcpConfig, -} from "./acpConfigs"; + baseSepoliaAcpConfigV2, +} from "./configs/acpConfigs"; import { ethFare, Fare, FareAmount, FareBigInt, wethFare } from "./acpFare"; import AcpError from "./acpError"; +import AcpContractClientV2 from "./contractClients/acpContractClientV2"; export default AcpClient; export { AcpError, IDeliverable, + BaseAcpContractClient, AcpContractClient, + AcpContractClientV2, AcpContractConfig, Fare, FareAmount, @@ -38,7 +47,9 @@ export { wethFare, ethFare, baseSepoliaAcpConfig, + baseSepoliaAcpConfigV2, baseAcpConfig, + baseAcpConfigV2, AcpJobPhases, MemoType, AcpJob, diff --git a/src/interfaces.ts b/src/interfaces.ts index 2df1b1b..b4e8ed8 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -1,5 +1,8 @@ import { Address } from "viem"; -import AcpContractClient, { AcpJobPhases, MemoType } from "./acpContractClient"; +import AcpContractClient, { + AcpJobPhases, + MemoType, +} from "./contractClients/baseAcpContractClient"; import AcpJob from "./acpJob"; import acpMemo from "./acpMemo"; @@ -33,6 +36,7 @@ export interface IAcpMemoData { signedReason?: string; expiry?: string; payableDetails?: PayableDetails; + contractAddress?: Address; } export interface IAcpMemo { data: IAcpMemoData; @@ -72,6 +76,7 @@ export interface IAcpJob { memos: IAcpMemoData[]; context: Record; createdAt: string; + contractAddress: Address; memoToSign?: number; }; error?: Error; @@ -90,7 +95,7 @@ export interface IAcpJobResponse { } export interface IAcpClientOptions { - acpContractClient: AcpContractClient; + acpContractClient: AcpContractClient | AcpContractClient[]; onNewTask?: (job: AcpJob, memoToSign?: acpMemo) => void; onEvaluate?: (job: AcpJob) => void; customRpcUrl?: string; @@ -109,12 +114,18 @@ export type AcpAgent = { ownerAddress: string; cluster: string | null; twitterHandle: string; - offerings: { + jobs: { name: string; price: number; - priceUsd: number; - requirementSchema?: Object; - deliverableSchema?: Object; + requirement?: Object | string; + deliverable?: Object | string; + }[]; + resources: { + name: string; + description: string; + url: string; + parameters?: Object; + id: number; }[]; symbol: string | null; virtualAgentId: string | null; @@ -125,6 +136,14 @@ export type AcpAgent = { minsFromLastOnline: number; isOnline: boolean; }; + contractAddress: Address; +}; + +export type IAcpAccount = { + id: number; + clientAddress: Address; + providerAddress: Address; + metadata: Record; }; export enum PayloadType {