From 07eda4400e2e75da7c3721a943d2b08fea472260 Mon Sep 17 00:00:00 2001 From: Zuhwa Date: Tue, 2 Sep 2025 16:19:42 +0800 Subject: [PATCH 01/41] SDK generalization --- src/acpClient.ts | 53 +++++++++++++++++--- src/acpFare.ts | 90 ++++++++++++++++++++++++--------- src/acpJob.ts | 112 +++++++++++++++++++++++++++++++++--------- src/acpJobOffering.ts | 6 +-- 4 files changed, 205 insertions(+), 56 deletions(-) diff --git a/src/acpClient.ts b/src/acpClient.ts index ac0b9ad..4775b31 100644 --- a/src/acpClient.ts +++ b/src/acpClient.ts @@ -20,8 +20,14 @@ import { IAcpMemo, IDeliverable, } from "./interfaces"; -import { ethFare, FareBigInt, IFareAmount, wethFare } from "./acpFare"; import AcpError from "./acpError"; +import { + ethFare, + FareAmount, + FareBigInt, + FareAmountBase, + wethFare, +} from "./acpFare"; const { version } = require("../package.json"); enum SocketEvents { @@ -235,7 +241,7 @@ class AcpClient { async initiateJob( providerAddress: Address, serviceRequirement: Object | string, - fareAmount: IFareAmount, + fareAmount: FareAmountBase, evaluatorAddress?: Address, expiredAt: Date = new Date(Date.now() + 1000 * 60 * 60 * 24) ) { @@ -270,6 +276,41 @@ 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 + ) { + 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, @@ -315,9 +356,9 @@ class AcpClient { async requestFunds( jobId: number, - transferFareAmount: IFareAmount, + transferFareAmount: FareAmountBase, recipient: Address, - feeFareAmount: IFareAmount, + feeFareAmount: FareAmountBase, feeType: FeeType, reason: GenericPayload, nextPhase: AcpJobPhases, @@ -355,9 +396,9 @@ class AcpClient { async transferFunds( jobId: number, - transferFareAmount: IFareAmount, + transferFareAmount: FareAmountBase, recipient: Address, - feeFareAmount: IFareAmount, + feeFareAmount: FareAmountBase, feeType: FeeType, reason: GenericPayload, nextPhase: AcpJobPhases, diff --git a/src/acpFare.ts b/src/acpFare.ts index 10568ca..0eacc9c 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 "./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..13c4be7 100644 --- a/src/acpJob.ts +++ b/src/acpJob.ts @@ -15,12 +15,15 @@ import { SwapTokenPayload, } 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 task: string | undefined; + public requirement: Record | string | undefined; + constructor( private acpClient: AcpClient, public id: number, @@ -34,47 +37,34 @@ class AcpJob { public context: Record ) { 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<{ + task: 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; - } - - public get serviceName() { - const content = this.memos.find( - (m) => m.nextPhase === AcpJobPhases.NEGOTIATION - )?.content; - - if (!content) { - return undefined; + if (contentObj.serviceName || contentObj.task) { + this.task = contentObj.task || contentObj.serviceName; } - - const contentObj = tryParseJson<{ - serviceName: string; - }>(content); - - return contentObj?.serviceName; } public get deliverable() { @@ -97,6 +87,80 @@ class AcpJob { return this.memos[this.memos.length - 1]; } + async createRequirementMemo(content: string) { + return await this.acpClient.createMemo( + this.id, + content, + AcpJobPhases.TRANSACTION + ); + } + + async createRequirementPayableMemo( + 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 + ) { + return await this.acpClient.createPayableMemo( + this.id, + content, + amount, + recipient, + AcpJobPhases.TRANSACTION, + type, + expiredAt + ); + } + + async payAndAcceptRequirement(reason?: string) { + const memo = this.memos.find( + (m) => m.nextPhase === AcpJobPhases.TRANSACTION + ); + + if (!memo) { + throw new Error("No transaction memo found"); + } + + const baseFareAmount = new FareAmount(this.price, this.baseFare); + const transferAmount = memo.payableDetails + ? await FareAmountBase.fromContractAddress( + memo.payableDetails.amount, + memo.payableDetails.token, + this.acpClient.acpContractClient.config + ) + : new FareAmount(0, this.baseFare); + + const totalAmount = + baseFareAmount.fare.contractAddress === + transferAmount.fare.contractAddress + ? baseFareAmount.add(transferAmount) + : baseFareAmount; + + await this.acpClient.acpContractClient.approveAllowance( + totalAmount.amount, + this.baseFare.contractAddress + ); + + if ( + baseFareAmount.fare.contractAddress !== + transferAmount.fare.contractAddress + ) { + await this.acpClient.acpContractClient.approveAllowance( + transferAmount.amount, + transferAmount.fare.contractAddress + ); + } + + await memo.sign(true, reason); + + return await this.acpClient.createMemo( + this.id, + `Payment made. ${reason ?? ""}`.trim(), + AcpJobPhases.EVALUATION + ); + } + async pay(amount: number, reason?: string) { const memo = this.memos.find( (m) => m.nextPhase === AcpJobPhases.TRANSACTION @@ -226,7 +290,7 @@ class AcpJob { async transferFunds( payload: GenericPayload, - fareAmount: IFareAmount, + fareAmount: FareAmountBase, walletAddress?: Address, expiredAt: Date = new Date(Date.now() + 1000 * 60 * 30) ) { diff --git a/src/acpJobOffering.ts b/src/acpJobOffering.ts index 303c264..59d6dab 100644 --- a/src/acpJobOffering.ts +++ b/src/acpJobOffering.ts @@ -32,18 +32,18 @@ class AcpJobOffering { } let finalServiceRequirement: Record = { - serviceName: this.name, + task: this.name, }; if (typeof serviceRequirement === "string") { finalServiceRequirement = { ...finalServiceRequirement, - message: serviceRequirement, + requirement: serviceRequirement, }; } else { finalServiceRequirement = { ...finalServiceRequirement, - serviceRequirement: serviceRequirement, + requirement: serviceRequirement, }; } From 0f8ca36ee46fb8755c1515ce3582c595e04024d1 Mon Sep 17 00:00:00 2001 From: Zuhwa Date: Wed, 3 Sep 2025 15:03:54 +0800 Subject: [PATCH 02/41] update naming for jobs --- src/acpJob.ts | 8 ++++---- src/acpJobOffering.ts | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/acpJob.ts b/src/acpJob.ts index 13c4be7..589912c 100644 --- a/src/acpJob.ts +++ b/src/acpJob.ts @@ -21,7 +21,7 @@ import AcpError from "./acpError"; class AcpJob { private baseFare: Fare; - public task: string | undefined; + public jobName: string | undefined; public requirement: Record | string | undefined; constructor( @@ -47,7 +47,7 @@ class AcpJob { } const contentObj = tryParseJson<{ - task: string; + jobName: string; requirement: Record | string; serviceName: string; serviceRequirement: Record; @@ -62,8 +62,8 @@ class AcpJob { contentObj.requirement || contentObj.serviceRequirement; } - if (contentObj.serviceName || contentObj.task) { - this.task = contentObj.task || contentObj.serviceName; + if (contentObj.serviceName || contentObj.jobName) { + this.jobName = contentObj.jobName || contentObj.serviceName; } } diff --git a/src/acpJobOffering.ts b/src/acpJobOffering.ts index 59d6dab..453d46e 100644 --- a/src/acpJobOffering.ts +++ b/src/acpJobOffering.ts @@ -12,7 +12,7 @@ class AcpJobOffering { public providerAddress: Address, public name: string, public price: number, - public requirementSchema?: Object + public requirement?: Object | string ) { this.ajv = new Ajv({ allErrors: true }); } @@ -22,8 +22,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) { @@ -32,7 +32,7 @@ class AcpJobOffering { } let finalServiceRequirement: Record = { - task: this.name, + jobName: this.name, }; if (typeof serviceRequirement === "string") { From 33a3691991cc335080641f5c7523aa909de58b79 Mon Sep 17 00:00:00 2001 From: Zuhwa Date: Mon, 8 Sep 2025 10:51:30 +0800 Subject: [PATCH 03/41] uses v3 search --- src/acpClient.ts | 10 +++++----- src/acpJob.ts | 8 ++++---- src/acpJobOffering.ts | 2 +- src/interfaces.ts | 14 ++++++++++---- 4 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/acpClient.ts b/src/acpClient.ts index 4775b31..2b1f049 100644 --- a/src/acpClient.ts +++ b/src/acpClient.ts @@ -186,7 +186,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(",")}`; @@ -222,13 +222,13 @@ class AcpClient { id: agent.id, name: agent.name, description: agent.description, - offerings: agent.offerings.map((offering) => { + jobOfferings: agent.jobs.map((jobs) => { return new AcpJobOffering( this, agent.walletAddress, - offering.name, - offering.priceUsd, - offering.requirementSchema + jobs.name, + jobs.price, + jobs.requirement ); }), twitterHandle: agent.twitterHandle, diff --git a/src/acpJob.ts b/src/acpJob.ts index 589912c..f25447f 100644 --- a/src/acpJob.ts +++ b/src/acpJob.ts @@ -21,7 +21,7 @@ import AcpError from "./acpError"; class AcpJob { private baseFare: Fare; - public jobName: string | undefined; + public name: string | undefined; public requirement: Record | string | undefined; constructor( @@ -47,7 +47,7 @@ class AcpJob { } const contentObj = tryParseJson<{ - jobName: string; + name: string; requirement: Record | string; serviceName: string; serviceRequirement: Record; @@ -62,8 +62,8 @@ class AcpJob { contentObj.requirement || contentObj.serviceRequirement; } - if (contentObj.serviceName || contentObj.jobName) { - this.jobName = contentObj.jobName || contentObj.serviceName; + if (contentObj.serviceName || contentObj.name) { + this.name = contentObj.name || contentObj.serviceName; } } diff --git a/src/acpJobOffering.ts b/src/acpJobOffering.ts index 453d46e..f5df1f7 100644 --- a/src/acpJobOffering.ts +++ b/src/acpJobOffering.ts @@ -32,7 +32,7 @@ class AcpJobOffering { } let finalServiceRequirement: Record = { - jobName: this.name, + name: this.name, }; if (typeof serviceRequirement === "string") { diff --git a/src/interfaces.ts b/src/interfaces.ts index 837a057..8e6b782 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -108,12 +108,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; From 15b26c4e07b1c10d554e5839d5b26180b729a959 Mon Sep 17 00:00:00 2001 From: Celeste Ang Date: Fri, 12 Sep 2025 03:28:58 +0800 Subject: [PATCH 04/41] SDK redesign - add node examples --- .../acp-base/self-evaluation-v2/.env.example | 6 + examples/acp-base/self-evaluation-v2/buyer.ts | 196 ++++++++++++++++++ examples/acp-base/self-evaluation-v2/env.ts | 37 ++++ .../acp-base/self-evaluation-v2/seller.ts | 175 ++++++++++++++++ 4 files changed, 414 insertions(+) create mode 100644 examples/acp-base/self-evaluation-v2/.env.example create mode 100644 examples/acp-base/self-evaluation-v2/buyer.ts create mode 100644 examples/acp-base/self-evaluation-v2/env.ts create mode 100644 examples/acp-base/self-evaluation-v2/seller.ts diff --git a/examples/acp-base/self-evaluation-v2/.env.example b/examples/acp-base/self-evaluation-v2/.env.example new file mode 100644 index 0000000..80ca9b7 --- /dev/null +++ b/examples/acp-base/self-evaluation-v2/.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/self-evaluation-v2/buyer.ts b/examples/acp-base/self-evaluation-v2/buyer.ts new file mode 100644 index 0000000..6f89640 --- /dev/null +++ b/examples/acp-base/self-evaluation-v2/buyer.ts @@ -0,0 +1,196 @@ +import * as readline from "readline"; +import AcpClient, { +AcpContractClient, + AcpJob, + AcpJobPhases, + AcpMemo, + baseSepoliaAcpConfig, + PayloadType, +} from "../../../src"; +import { + BUYER_AGENT_WALLET_ADDRESS, + WHITELISTED_WALLET_PRIVATE_KEY, + BUYER_ENTITY_ID, +} from "./env"; + +async function sleep(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +async function main() { + let currentJob: AcpJob | null = null; + const config = baseSepoliaAcpConfig; + + const acpClient = new AcpClient({ + acpContractClient: await AcpContractClient.build( + WHITELISTED_WALLET_PRIVATE_KEY, + BUYER_ENTITY_ID, + BUYER_AGENT_WALLET_ADDRESS, + config + ), + onNewTask: async (job: AcpJob, memoToSign?: AcpMemo) => { + console.log("New job", job.id, memoToSign?.id); + + if (job.phase === AcpJobPhases.NEGOTIATION) { + console.log("Pay to job"); + await job.pay(0); + currentJob = job; + return; + } + + currentJob = job; + + if (job.phase !== AcpJobPhases.TRANSACTION) { + console.log("Job is not in transaction phase"); + return; + } + + if (!memoToSign) { + console.log("No memo to sign"); + return; + } + + switch (memoToSign.payloadType) { + case PayloadType.CLOSE_JOB_AND_WITHDRAW: + await job.confirmJobClosure(memoToSign.id, true); + console.log("Closed job"); + break; + + case PayloadType.RESPONSE_SWAP_TOKEN: + await memoToSign.sign(true, "accepts swap token"); + console.log("Swapped token"); + break; + + case PayloadType.CLOSE_POSITION: + await job.confirmClosePosition(memoToSign.id, true); + console.log("Closed position"); + break; + + default: + console.log("Unhandled payload type", memoToSign.payloadType); + } + }, + }); + + console.log("Initiating job"); + + const agents = await acpClient.browseAgents("", {}); + console.log(agents); + console.log(agents[0].jobOfferings); + + agents[0].jobOfferings[0].price = 0; + const jobId = await agents[0].jobOfferings[0].initiateJob("Help me trade"); + console.log("Job initiated", jobId); + + const actionsDefinition = [ + { + index: 1, + desc: "Open position", + action: async () => { + const result = await currentJob?.openPosition( + [ + { + symbol: "BTC", + amount: 0.001, // amount in $VIRTUAL + tp: { percentage: 5 }, + sl: { percentage: 2 }, + }, + { + symbol: "ETH", + amount: 0.002, // amount in $VIRTUAL + tp: { percentage: 10 }, + sl: { percentage: 5 }, + }, + ], + 0.001, // fee amount in $VIRTUAL + new Date(Date.now() + 1000 * 60 * 3) // 3 minutes + ); + console.log("Opening position result", result); + }, + }, + { + index: 2, + desc: "Swap token", + action: async () => { + const result = await currentJob?.swapToken( + { + fromSymbol: "BMW", + fromContractAddress: + "0xbfAB80ccc15DF6fb7185f9498d6039317331846a", // BMW token address + amount: 0.01, + toSymbol: "USDC", + }, + 18, // decimals from BMW + 0.001 // fee amount in $USDC + ); + console.log("Swapping token result", result); + }, + }, + { + index: 3, + desc: "Close partial position", + action: async () => { + const result = await currentJob?.closePartialPosition({ + positionId: 0, + amount: 1, + }); + console.log("Closing partial position result", result); + }, + }, + { + index: 4, + desc: "Close position", + action: async () => { + const result = await currentJob?.requestClosePosition({ + positionId: 0, + }); + console.log("Closing position result", result); + }, + }, + { + index: 5, + desc: "Close job", + action: async () => { + const result = await currentJob?.closeJob(); + console.log("Closing job result", result); + }, + }, + ]; + + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + const question = (prompt: string): Promise => + new Promise((resolve) => rl.question(prompt, resolve)); + + while (true) { + await sleep(5000); + + if (!currentJob) { + console.log("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("Select an action (enter the number): "); + 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/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..d7750a9 --- /dev/null +++ b/examples/acp-base/self-evaluation-v2/seller.ts @@ -0,0 +1,175 @@ +import dotenv from "dotenv"; +import AcpClient, { + AcpContractClient, + AcpJob, + AcpJobPhases, + AcpMemo, + baseSepoliaAcpConfig, + FareAmount, + MemoType, +} from "../../../src"; +import { Address } from "viem"; +import { + SELLER_AGENT_WALLET_ADDRESS, + SELLER_ENTITY_ID, + WHITELISTED_WALLET_PRIVATE_KEY +} from "./env"; + +dotenv.config(); + +const config = baseSepoliaAcpConfig; + +enum TaskType { + OPEN_POSITION = "open_position", + CLOSE_POSITION = "close_position", + SWAP_TOKEN = "swap_token", + WITHDRAW = "withdraw", +} + +interface IPosition { + symbol: string; + amount: number; +} + +interface IClientWallet { + address: Address; + assets: FareAmount[]; + positions: IPosition[]; +} + +const client: Record = {}; + +const onNewTask = async (job: AcpJob, memoToSign?: AcpMemo) => { + if (!client[job.clientAddress]) { + client[job.clientAddress] = { + address: job.clientAddress, + assets: [], + positions: [], + }; + } + + if (job.phase === AcpJobPhases.REQUEST) { + return await handleTaskRequest(job, memoToSign); + } + + if (job.phase === AcpJobPhases.TRANSACTION) { + return await handleTaskTransaction(job, memoToSign); + } + + console.error("Job is not in request or transaction phase", job.phase); + return; +}; + +const handleTaskRequest = async (job: AcpJob, memoToSign?: AcpMemo) => { + const task = memoToSign?.payloadType; + if (!task) { + console.error("Task not found", memoToSign?.payloadType); + return; + } + + if (task === TaskType.OPEN_POSITION.toString()) { + await memoToSign.sign(true, "accepts open position"); + return await job.createRequirementPayableMemo( + "Send me 1 USDC to open position", + MemoType.PAYABLE_REQUEST, + new FareAmount(1, config.baseFare), + job.providerAddress + ); + } + + if (task === TaskType.CLOSE_POSITION.toString()) { + await memoToSign.sign(true, "accepts close position"); + return await job.createRequirementMemo("Closing a random position"); + } + + if (task === TaskType.SWAP_TOKEN.toString()) { + await memoToSign.sign(true, "accepts swap token"); + return await job.createRequirementPayableMemo( + "Send me 1 USDC to swap to 1 USD", + MemoType.PAYABLE_REQUEST, + new FareAmount(1, config.baseFare), + job.providerAddress + ); + } + + if (task === TaskType.WITHDRAW.toString()) { + await memoToSign.sign(true, "accepts withdraw"); + return await job.createRequirementPayableMemo( + "Withdrawing a random amount", + MemoType.PAYABLE_TRANSFER_ESCROW, + new FareAmount(1, config.baseFare), + job.providerAddress + ); + } + + console.error("Task not supported", task); + return; +}; + +const handleTaskTransaction = async (job: AcpJob, memoToSign?: AcpMemo) => { + const task = memoToSign?.payloadType; + if (!task) { + console.error("Task not found", memoToSign?.payloadType); + return; + } + + if (task === TaskType.OPEN_POSITION.toString()) { + client[job.clientAddress].positions.push({ + symbol: "USDC", + amount: 1, + }); + + await job.deliver({ + type: "message", + value: "Opened position with hash 0x1234567890", + }); + return; + } + + if (task === TaskType.CLOSE_POSITION.toString()) { + client[job.clientAddress].positions = client[ + job.clientAddress + ].positions.filter((p) => p.symbol !== "USDC"); + + await job.deliver({ + type: "message", + value: "Closed position with hash 0x1234567890", + }); + return; + } + + if (task === TaskType.SWAP_TOKEN.toString()) { + client[job.clientAddress].assets.push(new FareAmount(1, config.baseFare)); + + await job.deliver({ + type: "message", + value: "Swapped token with hash 0x1234567890", + }); + return; + } + + if (task === TaskType.WITHDRAW.toString()) { + await job.deliver({ + type: "message", + value: "Withdrawn amount with hash 0x1234567890", + }); + return; + } + + console.error("Task not supported", task); + return; +}; + +async function main() { + const acpClient = new AcpClient({ + acpContractClient: await AcpContractClient.build( + WHITELISTED_WALLET_PRIVATE_KEY, + SELLER_ENTITY_ID, + SELLER_AGENT_WALLET_ADDRESS, + config + ), + onNewTask, + }); +} + +main(); From 99703a1ead79680f8dec77dea59d911571b8bb94 Mon Sep 17 00:00:00 2001 From: Celeste Ang Date: Fri, 12 Sep 2025 03:31:50 +0800 Subject: [PATCH 05/41] SDK redesign - fix node example formatting --- examples/acp-base/self-evaluation-v2/buyer.ts | 22 ++++++++-------- .../acp-base/self-evaluation-v2/seller.ts | 26 +++++++++---------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/examples/acp-base/self-evaluation-v2/buyer.ts b/examples/acp-base/self-evaluation-v2/buyer.ts index 6f89640..01995d2 100644 --- a/examples/acp-base/self-evaluation-v2/buyer.ts +++ b/examples/acp-base/self-evaluation-v2/buyer.ts @@ -90,16 +90,16 @@ async function main() { const result = await currentJob?.openPosition( [ { - symbol: "BTC", - amount: 0.001, // amount in $VIRTUAL - tp: { percentage: 5 }, - sl: { percentage: 2 }, + symbol: "BTC", + amount: 0.001, // amount in $VIRTUAL + tp: { percentage: 5 }, + sl: { percentage: 2 }, }, { - symbol: "ETH", - amount: 0.002, // amount in $VIRTUAL - tp: { percentage: 10 }, - sl: { percentage: 5 }, + symbol: "ETH", + amount: 0.002, // amount in $VIRTUAL + tp: { percentage: 10 }, + sl: { percentage: 5 }, }, ], 0.001, // fee amount in $VIRTUAL @@ -131,8 +131,8 @@ async function main() { desc: "Close partial position", action: async () => { const result = await currentJob?.closePartialPosition({ - positionId: 0, - amount: 1, + positionId: 0, + amount: 1, }); console.log("Closing partial position result", result); }, @@ -142,7 +142,7 @@ async function main() { desc: "Close position", action: async () => { const result = await currentJob?.requestClosePosition({ - positionId: 0, + positionId: 0, }); console.log("Closing position result", result); }, diff --git a/examples/acp-base/self-evaluation-v2/seller.ts b/examples/acp-base/self-evaluation-v2/seller.ts index d7750a9..78dc82d 100644 --- a/examples/acp-base/self-evaluation-v2/seller.ts +++ b/examples/acp-base/self-evaluation-v2/seller.ts @@ -95,10 +95,10 @@ const handleTaskRequest = async (job: AcpJob, memoToSign?: AcpMemo) => { if (task === TaskType.WITHDRAW.toString()) { await memoToSign.sign(true, "accepts withdraw"); return await job.createRequirementPayableMemo( - "Withdrawing a random amount", - MemoType.PAYABLE_TRANSFER_ESCROW, - new FareAmount(1, config.baseFare), - job.providerAddress + "Withdrawing a random amount", + MemoType.PAYABLE_TRANSFER_ESCROW, + new FareAmount(1, config.baseFare), + job.providerAddress ); } @@ -115,13 +115,13 @@ const handleTaskTransaction = async (job: AcpJob, memoToSign?: AcpMemo) => { if (task === TaskType.OPEN_POSITION.toString()) { client[job.clientAddress].positions.push({ - symbol: "USDC", - amount: 1, + symbol: "USDC", + amount: 1, }); await job.deliver({ - type: "message", - value: "Opened position with hash 0x1234567890", + type: "message", + value: "Opened position with hash 0x1234567890", }); return; } @@ -142,16 +142,16 @@ const handleTaskTransaction = async (job: AcpJob, memoToSign?: AcpMemo) => { client[job.clientAddress].assets.push(new FareAmount(1, config.baseFare)); await job.deliver({ - type: "message", - value: "Swapped token with hash 0x1234567890", + type: "message", + value: "Swapped token with hash 0x1234567890", }); return; } if (task === TaskType.WITHDRAW.toString()) { - await job.deliver({ - type: "message", - value: "Withdrawn amount with hash 0x1234567890", + await job.deliver({ + type: "message", + value: "Withdrawn amount with hash 0x1234567890", }); return; } From a95d9b117a0b0d13dd20b80d8f91165ede57b229 Mon Sep 17 00:00:00 2001 From: Celeste Ang Date: Fri, 12 Sep 2025 03:34:38 +0800 Subject: [PATCH 06/41] SDK redesign - fix node example formatting --- .../acp-base/self-evaluation-v2/seller.ts | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/examples/acp-base/self-evaluation-v2/seller.ts b/examples/acp-base/self-evaluation-v2/seller.ts index 78dc82d..65123f7 100644 --- a/examples/acp-base/self-evaluation-v2/seller.ts +++ b/examples/acp-base/self-evaluation-v2/seller.ts @@ -83,12 +83,12 @@ const handleTaskRequest = async (job: AcpJob, memoToSign?: AcpMemo) => { } if (task === TaskType.SWAP_TOKEN.toString()) { - await memoToSign.sign(true, "accepts swap token"); - return await job.createRequirementPayableMemo( - "Send me 1 USDC to swap to 1 USD", - MemoType.PAYABLE_REQUEST, - new FareAmount(1, config.baseFare), - job.providerAddress + await memoToSign.sign(true, "accepts swap token"); + return await job.createRequirementPayableMemo( + "Send me 1 USDC to swap to 1 USD", + MemoType.PAYABLE_REQUEST, + new FareAmount(1, config.baseFare), + job.providerAddress ); } @@ -128,12 +128,12 @@ const handleTaskTransaction = async (job: AcpJob, memoToSign?: AcpMemo) => { if (task === TaskType.CLOSE_POSITION.toString()) { client[job.clientAddress].positions = client[ - job.clientAddress + job.clientAddress ].positions.filter((p) => p.symbol !== "USDC"); await job.deliver({ - type: "message", - value: "Closed position with hash 0x1234567890", + type: "message", + value: "Closed position with hash 0x1234567890", }); return; } @@ -149,7 +149,7 @@ const handleTaskTransaction = async (job: AcpJob, memoToSign?: AcpMemo) => { } if (task === TaskType.WITHDRAW.toString()) { - await job.deliver({ + await job.deliver({ type: "message", value: "Withdrawn amount with hash 0x1234567890", }); From b1637212daaf8237b61982b1d83401456f3bac17 Mon Sep 17 00:00:00 2001 From: jxx016 Date: Fri, 12 Sep 2025 18:08:54 -0500 Subject: [PATCH 07/41] adding readme --- .../acp-base/self-evaluation-v2/README.md | 132 ++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 examples/acp-base/self-evaluation-v2/README.md 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..6bbdc5e --- /dev/null +++ b/examples/acp-base/self-evaluation-v2/README.md @@ -0,0 +1,132 @@ +# 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 +- **Withdrawal Operations**: Custom withdrawal job implementations + +## 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 +- **Withdraw Funds**: Request fund withdrawals +- **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 +- `WITHDRAW`: Process withdrawal requests + +## 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) + - Withdraw funds from positions + - 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. Withdraw + 5. Close job + ``` + +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 From 52f25331c34c2e36ac5054cdbf41ea87e13d4922 Mon Sep 17 00:00:00 2001 From: jxx016 Date: Tue, 16 Sep 2025 15:50:55 -0500 Subject: [PATCH 08/41] adding changes to buyer.ts example file --- examples/acp-base/self-evaluation-v2/buyer.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/examples/acp-base/self-evaluation-v2/buyer.ts b/examples/acp-base/self-evaluation-v2/buyer.ts index 01995d2..62ae852 100644 --- a/examples/acp-base/self-evaluation-v2/buyer.ts +++ b/examples/acp-base/self-evaluation-v2/buyer.ts @@ -1,9 +1,12 @@ import * as readline from "readline"; import AcpClient, { + AcpAgentSort, AcpContractClient, + AcpGraduationStatus, AcpJob, AcpJobPhases, AcpMemo, + AcpOnlineStatus, baseSepoliaAcpConfig, PayloadType, } from "../../../src"; @@ -74,7 +77,15 @@ async function main() { console.log("Initiating job"); - const agents = await acpClient.browseAgents("", {}); + const agents = await acpClient.browseAgents( + "blue-dot-testnet", + { + sort_by: [AcpAgentSort.SUCCESSFUL_JOB_COUNT], + top_k: 5, + graduationStatus: AcpGraduationStatus.ALL, + onlineStatus: AcpOnlineStatus.ALL, + } + ); console.log(agents); console.log(agents[0].jobOfferings); From f11d845ded46956c44be1af34c368e112b8e0abb Mon Sep 17 00:00:00 2001 From: jxx016 Date: Tue, 16 Sep 2025 16:40:06 -0500 Subject: [PATCH 09/41] add changes to seller.ts file --- .../acp-base/self-evaluation-v2/seller.ts | 102 ++++++++++++------ 1 file changed, 72 insertions(+), 30 deletions(-) diff --git a/examples/acp-base/self-evaluation-v2/seller.ts b/examples/acp-base/self-evaluation-v2/seller.ts index 65123f7..5bbcb78 100644 --- a/examples/acp-base/self-evaluation-v2/seller.ts +++ b/examples/acp-base/self-evaluation-v2/seller.ts @@ -9,6 +9,7 @@ import AcpClient, { MemoType, } from "../../../src"; import { Address } from "viem"; +import { createHash } from "crypto"; import { SELLER_AGENT_WALLET_ADDRESS, SELLER_ENTITY_ID, @@ -19,7 +20,7 @@ dotenv.config(); const config = baseSepoliaAcpConfig; -enum TaskType { +enum JobName { OPEN_POSITION = "open_position", CLOSE_POSITION = "close_position", SWAP_TOKEN = "swap_token", @@ -32,22 +33,31 @@ interface IPosition { } interface IClientWallet { - address: Address; + clientAddress: Address; assets: FareAmount[]; positions: IPosition[]; } const client: Record = {}; -const onNewTask = async (job: AcpJob, memoToSign?: AcpMemo) => { - if (!client[job.clientAddress]) { - client[job.clientAddress] = { - address: job.clientAddress, +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, assets: [], positions: [], }; } + return client[walletAddress]; +}; + +const onNewTask = async (job: AcpJob, memoToSign?: AcpMemo) => { + const wallet = getClientWallet(job.clientAddress); + if (job.phase === AcpJobPhases.REQUEST) { return await handleTaskRequest(job, memoToSign); } @@ -61,13 +71,18 @@ const onNewTask = async (job: AcpJob, memoToSign?: AcpMemo) => { }; const handleTaskRequest = async (job: AcpJob, memoToSign?: AcpMemo) => { - const task = memoToSign?.payloadType; - if (!task) { - console.error("Task not found", memoToSign?.payloadType); + if (!memoToSign) { + console.error("Memo to sign not found", memoToSign); return; } - if (task === TaskType.OPEN_POSITION.toString()) { + const jobName = job.name; + if (!jobName) { + console.error("job name not found", job); + return; + } + + if (jobName === JobName.OPEN_POSITION.toString()) { await memoToSign.sign(true, "accepts open position"); return await job.createRequirementPayableMemo( "Send me 1 USDC to open position", @@ -77,12 +92,12 @@ const handleTaskRequest = async (job: AcpJob, memoToSign?: AcpMemo) => { ); } - if (task === TaskType.CLOSE_POSITION.toString()) { + if (jobName === JobName.CLOSE_POSITION.toString()) { await memoToSign.sign(true, "accepts close position"); return await job.createRequirementMemo("Closing a random position"); } - if (task === TaskType.SWAP_TOKEN.toString()) { + if (jobName === JobName.SWAP_TOKEN.toString()) { await memoToSign.sign(true, "accepts swap token"); return await job.createRequirementPayableMemo( "Send me 1 USDC to swap to 1 USD", @@ -92,7 +107,7 @@ const handleTaskRequest = async (job: AcpJob, memoToSign?: AcpMemo) => { ); } - if (task === TaskType.WITHDRAW.toString()) { + if (jobName === JobName.WITHDRAW.toString()) { await memoToSign.sign(true, "accepts withdraw"); return await job.createRequirementPayableMemo( "Withdrawing a random amount", @@ -102,22 +117,30 @@ const handleTaskRequest = async (job: AcpJob, memoToSign?: AcpMemo) => { ); } - console.error("Task not supported", task); + console.error("Job name not supported", jobName); return; }; const handleTaskTransaction = async (job: AcpJob, memoToSign?: AcpMemo) => { - const task = memoToSign?.payloadType; - if (!task) { - console.error("Task not found", memoToSign?.payloadType); + const jobName = job.name; + if (!jobName) { + console.error("job name not found", job); return; } - if (task === TaskType.OPEN_POSITION.toString()) { - client[job.clientAddress].positions.push({ - symbol: "USDC", - amount: 1, - }); + if (jobName === JobName.OPEN_POSITION.toString()) { + const wallet = getClientWallet(job.clientAddress); + + const position = wallet.positions.find((p) => p.symbol === "USDC"); + + if (position) { + position.amount += 1; + } else { + wallet.positions.push({ + symbol: "USDC", + amount: 1, + }); + } await job.deliver({ type: "message", @@ -126,10 +149,21 @@ const handleTaskTransaction = async (job: AcpJob, memoToSign?: AcpMemo) => { return; } - if (task === TaskType.CLOSE_POSITION.toString()) { - client[job.clientAddress].positions = client[ - job.clientAddress - ].positions.filter((p) => p.symbol !== "USDC"); + if (jobName === JobName.CLOSE_POSITION.toString()) { + const wallet = getClientWallet(job.clientAddress); + const position = wallet.positions.find((p) => p.symbol === "USDC"); + wallet.positions = wallet.positions.filter((p) => p.symbol !== "USDC"); + + const asset = wallet.assets.find( + (a) => a.fare.contractAddress === config.baseFare.contractAddress + ); + if (!asset) { + wallet.assets.push( + new FareAmount(position?.amount || 0, config.baseFare) + ); + } else { + asset.amount += BigInt(position?.amount || 0); + } await job.deliver({ type: "message", @@ -138,8 +172,16 @@ const handleTaskTransaction = async (job: AcpJob, memoToSign?: AcpMemo) => { return; } - if (task === TaskType.SWAP_TOKEN.toString()) { - client[job.clientAddress].assets.push(new FareAmount(1, config.baseFare)); + if (jobName === JobName.SWAP_TOKEN.toString()) { + const wallet = getClientWallet(job.clientAddress); + const asset = wallet.assets.find( + (a) => a.fare.contractAddress === config.baseFare.contractAddress + ); + if (!asset) { + wallet.assets.push(new FareAmount(1, config.baseFare)); + } else { + asset.amount += BigInt(1); + } await job.deliver({ type: "message", @@ -148,7 +190,7 @@ const handleTaskTransaction = async (job: AcpJob, memoToSign?: AcpMemo) => { return; } - if (task === TaskType.WITHDRAW.toString()) { + if (jobName === JobName.WITHDRAW.toString()) { await job.deliver({ type: "message", value: "Withdrawn amount with hash 0x1234567890", @@ -156,7 +198,7 @@ const handleTaskTransaction = async (job: AcpJob, memoToSign?: AcpMemo) => { return; } - console.error("Task not supported", task); + console.error("Job name not supported", jobName); return; }; From 88316a49977b82e0afefcf6370a8b8d7d38d631d Mon Sep 17 00:00:00 2001 From: Celeste Ang Date: Wed, 17 Sep 2025 11:02:59 +0800 Subject: [PATCH 10/41] buyer changes --- examples/acp-base/self-evaluation-v2/buyer.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/examples/acp-base/self-evaluation-v2/buyer.ts b/examples/acp-base/self-evaluation-v2/buyer.ts index 62ae852..baa6dbd 100644 --- a/examples/acp-base/self-evaluation-v2/buyer.ts +++ b/examples/acp-base/self-evaluation-v2/buyer.ts @@ -78,7 +78,7 @@ async function main() { console.log("Initiating job"); const agents = await acpClient.browseAgents( - "blue-dot-testnet", + "calm_seller", { sort_by: [AcpAgentSort.SUCCESSFUL_JOB_COUNT], top_k: 5, @@ -90,7 +90,13 @@ async function main() { console.log(agents[0].jobOfferings); agents[0].jobOfferings[0].price = 0; - const jobId = await agents[0].jobOfferings[0].initiateJob("Help me trade"); + const jobId = await agents[0].jobOfferings[0].initiateJob({ + "fromSymbol": "USDC", + "fromContractAddress": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", // USDC token address + "amount": 0.01, + "toSymbol": "BMW", + "toContractAddress": "0xbfAB80ccc15DF6fb7185f9498d6039317331846a" // BMW token address + }); console.log("Job initiated", jobId); const actionsDefinition = [ From 0cc4047340c6e8cc8dfdbd46b7997a1c133e8366 Mon Sep 17 00:00:00 2001 From: Celeste Ang Date: Wed, 17 Sep 2025 14:28:24 +0800 Subject: [PATCH 11/41] rename example folder and add payjob helper --- .../.env.example | 0 .../README.md | 0 .../{self-evaluation-v2 => funds-v2}/buyer.ts | 123 +++++++++--------- .../{self-evaluation-v2 => funds-v2}/env.ts | 0 .../seller.ts | 0 5 files changed, 61 insertions(+), 62 deletions(-) rename examples/acp-base/{self-evaluation-v2 => funds-v2}/.env.example (100%) rename examples/acp-base/{self-evaluation-v2 => funds-v2}/README.md (100%) rename examples/acp-base/{self-evaluation-v2 => funds-v2}/buyer.ts (63%) rename examples/acp-base/{self-evaluation-v2 => funds-v2}/env.ts (100%) rename examples/acp-base/{self-evaluation-v2 => funds-v2}/seller.ts (100%) diff --git a/examples/acp-base/self-evaluation-v2/.env.example b/examples/acp-base/funds-v2/.env.example similarity index 100% rename from examples/acp-base/self-evaluation-v2/.env.example rename to examples/acp-base/funds-v2/.env.example diff --git a/examples/acp-base/self-evaluation-v2/README.md b/examples/acp-base/funds-v2/README.md similarity index 100% rename from examples/acp-base/self-evaluation-v2/README.md rename to examples/acp-base/funds-v2/README.md diff --git a/examples/acp-base/self-evaluation-v2/buyer.ts b/examples/acp-base/funds-v2/buyer.ts similarity index 63% rename from examples/acp-base/self-evaluation-v2/buyer.ts rename to examples/acp-base/funds-v2/buyer.ts index baa6dbd..fb84613 100644 --- a/examples/acp-base/self-evaluation-v2/buyer.ts +++ b/examples/acp-base/funds-v2/buyer.ts @@ -36,7 +36,7 @@ async function main() { if (job.phase === AcpJobPhases.NEGOTIATION) { console.log("Pay to job"); - await job.pay(0); + await job.payAndAcceptRequirement("I accept the job requirements"); currentJob = job; return; } @@ -89,7 +89,6 @@ async function main() { console.log(agents); console.log(agents[0].jobOfferings); - agents[0].jobOfferings[0].price = 0; const jobId = await agents[0].jobOfferings[0].initiateJob({ "fromSymbol": "USDC", "fromContractAddress": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", // USDC token address @@ -101,76 +100,76 @@ async function main() { const actionsDefinition = [ { - index: 1, - desc: "Open position", - action: async () => { - const result = await currentJob?.openPosition( - [ - { - symbol: "BTC", - amount: 0.001, // amount in $VIRTUAL - tp: { percentage: 5 }, - sl: { percentage: 2 }, - }, - { - symbol: "ETH", - amount: 0.002, // amount in $VIRTUAL - tp: { percentage: 10 }, - sl: { percentage: 5 }, - }, - ], - 0.001, // fee amount in $VIRTUAL - new Date(Date.now() + 1000 * 60 * 3) // 3 minutes - ); - console.log("Opening position result", result); + index: 1, + desc: "Open position", + action: async () => { + const result = await currentJob?.openPosition( + [ + { + symbol: "BTC", + amount: 0.001, // amount in $VIRTUAL + tp: { percentage: 5 }, + sl: { percentage: 2 }, + }, + { + symbol: "ETH", + amount: 0.002, // amount in $VIRTUAL + tp: { percentage: 10 }, + sl: { percentage: 5 }, + }, + ], + 0.001, // fee amount in $VIRTUAL + new Date(Date.now() + 1000 * 60 * 3) // 3 minutes + ); + console.log("Opening position result", result); }, }, { - index: 2, - desc: "Swap token", - action: async () => { - const result = await currentJob?.swapToken( - { - fromSymbol: "BMW", - fromContractAddress: - "0xbfAB80ccc15DF6fb7185f9498d6039317331846a", // BMW token address - amount: 0.01, - toSymbol: "USDC", + index: 2, + desc: "Swap token", + action: async () => { + const result = await currentJob?.swapToken( + { + fromSymbol: "BMW", + fromContractAddress: + "0xbfAB80ccc15DF6fb7185f9498d6039317331846a", // BMW token address + amount: 0.01, + toSymbol: "USDC", + }, + 18, // decimals from BMW + 0.001 // fee amount in $USDC + ); + console.log("Swapping token result", result); }, - 18, // decimals from BMW - 0.001 // fee amount in $USDC - ); - console.log("Swapping token result", result); - }, }, { - index: 3, - desc: "Close partial position", - action: async () => { - const result = await currentJob?.closePartialPosition({ - positionId: 0, - amount: 1, - }); - console.log("Closing partial position result", result); - }, + index: 3, + desc: "Close partial position", + action: async () => { + const result = await currentJob?.closePartialPosition({ + positionId: 0, + amount: 1, + }); + console.log("Closing partial position result", result); + }, }, { - index: 4, - desc: "Close position", - action: async () => { - const result = await currentJob?.requestClosePosition({ - positionId: 0, - }); - console.log("Closing position result", result); - }, + index: 4, + desc: "Close position", + action: async () => { + const result = await currentJob?.requestClosePosition({ + positionId: 0, + }); + console.log("Closing position result", result); + }, }, { - index: 5, - desc: "Close job", - action: async () => { - const result = await currentJob?.closeJob(); - console.log("Closing job result", result); - }, + index: 5, + desc: "Close job", + action: async () => { + const result = await currentJob?.closeJob(); + console.log("Closing job result", result); + }, }, ]; diff --git a/examples/acp-base/self-evaluation-v2/env.ts b/examples/acp-base/funds-v2/env.ts similarity index 100% rename from examples/acp-base/self-evaluation-v2/env.ts rename to examples/acp-base/funds-v2/env.ts diff --git a/examples/acp-base/self-evaluation-v2/seller.ts b/examples/acp-base/funds-v2/seller.ts similarity index 100% rename from examples/acp-base/self-evaluation-v2/seller.ts rename to examples/acp-base/funds-v2/seller.ts From 6625c9db7abb9c06bf62db42004923670878883e Mon Sep 17 00:00:00 2001 From: Celeste Ang Date: Wed, 17 Sep 2025 14:48:18 +0800 Subject: [PATCH 12/41] refactor: formatting and tweaks to get token swap example working --- examples/acp-base/funds-v2/buyer.ts | 163 +++++++-------------------- examples/acp-base/funds-v2/seller.ts | 8 +- 2 files changed, 45 insertions(+), 126 deletions(-) diff --git a/examples/acp-base/funds-v2/buyer.ts b/examples/acp-base/funds-v2/buyer.ts index fb84613..3883998 100644 --- a/examples/acp-base/funds-v2/buyer.ts +++ b/examples/acp-base/funds-v2/buyer.ts @@ -53,25 +53,25 @@ async function main() { return; } - switch (memoToSign.payloadType) { - case PayloadType.CLOSE_JOB_AND_WITHDRAW: - await job.confirmJobClosure(memoToSign.id, true); - console.log("Closed job"); - break; - - case PayloadType.RESPONSE_SWAP_TOKEN: - await memoToSign.sign(true, "accepts swap token"); - console.log("Swapped token"); - break; - - case PayloadType.CLOSE_POSITION: - await job.confirmClosePosition(memoToSign.id, true); - console.log("Closed position"); - break; - - default: - console.log("Unhandled payload type", memoToSign.payloadType); - } + // switch (memoToSign.payloadType) { + // case PayloadType.CLOSE_JOB_AND_WITHDRAW: + // await job.confirmJobClosure(memoToSign.id, true); + // console.log("Closed job"); + // break; + + // case PayloadType.RESPONSE_SWAP_TOKEN: + // await memoToSign.sign(true, "accepts swap token"); + // console.log("Swapped token"); + // break; + + // case PayloadType.CLOSE_POSITION: + // await job.confirmClosePosition(memoToSign.id, true); + // console.log("Closed position"); + // break; + + // default: + // console.log("Unhandled payload type", memoToSign.payloadType); + // } }, }); @@ -98,115 +98,32 @@ async function main() { }); console.log("Job initiated", jobId); - const actionsDefinition = [ - { - index: 1, - desc: "Open position", - action: async () => { - const result = await currentJob?.openPosition( - [ - { - symbol: "BTC", - amount: 0.001, // amount in $VIRTUAL - tp: { percentage: 5 }, - sl: { percentage: 2 }, - }, - { - symbol: "ETH", - amount: 0.002, // amount in $VIRTUAL - tp: { percentage: 10 }, - sl: { percentage: 5 }, - }, - ], - 0.001, // fee amount in $VIRTUAL - new Date(Date.now() + 1000 * 60 * 3) // 3 minutes - ); - console.log("Opening position result", result); - }, - }, - { - index: 2, - desc: "Swap token", - action: async () => { - const result = await currentJob?.swapToken( - { - fromSymbol: "BMW", - fromContractAddress: - "0xbfAB80ccc15DF6fb7185f9498d6039317331846a", // BMW token address - amount: 0.01, - toSymbol: "USDC", - }, - 18, // decimals from BMW - 0.001 // fee amount in $USDC - ); - console.log("Swapping token result", result); - }, - }, - { - index: 3, - desc: "Close partial position", - action: async () => { - const result = await currentJob?.closePartialPosition({ - positionId: 0, - amount: 1, - }); - console.log("Closing partial position result", result); - }, - }, - { - index: 4, - desc: "Close position", - action: async () => { - const result = await currentJob?.requestClosePosition({ - positionId: 0, - }); - console.log("Closing position result", result); - }, - }, - { - index: 5, - desc: "Close job", - action: async () => { - const result = await currentJob?.closeJob(); - console.log("Closing job result", result); - }, - }, - ]; - - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, - }); - - const question = (prompt: string): Promise => - new Promise((resolve) => rl.question(prompt, resolve)); - - while (true) { - await sleep(5000); + // while (true) { + // await sleep(5000); - if (!currentJob) { - console.log("No job found, waiting for new job"); - continue; - } + // if (!currentJob) { + // console.log("No job found, waiting for new job"); + // continue; + // } - console.log("\nAvailable actions:"); - actionsDefinition.forEach((action) => { - console.log(`${action.index}. ${action.desc}`); - }); + // console.log("\nAvailable actions:"); + // actionsDefinition.forEach((action) => { + // console.log(`${action.index}. ${action.desc}`); + // }); - const answer = await question("Select an action (enter the number): "); - const selectedIndex = parseInt(answer, 10); + // const answer = await question("Select an action (enter the number): "); + // const selectedIndex = parseInt(answer, 10); - const selectedAction = actionsDefinition.find( - (action) => action.index === selectedIndex - ); + // const selectedAction = actionsDefinition.find( + // (action) => action.index === selectedIndex + // ); - if (selectedAction) { - await selectedAction.action(); - } else { - console.log("Invalid selection. Please try again."); - } - } + // if (selectedAction) { + // await selectedAction.action(); + // } else { + // console.log("Invalid selection. Please try again."); + // } + // } } main(); diff --git a/examples/acp-base/funds-v2/seller.ts b/examples/acp-base/funds-v2/seller.ts index 5bbcb78..e8e20a7 100644 --- a/examples/acp-base/funds-v2/seller.ts +++ b/examples/acp-base/funds-v2/seller.ts @@ -59,10 +59,12 @@ const onNewTask = async (job: AcpJob, memoToSign?: AcpMemo) => { const wallet = getClientWallet(job.clientAddress); if (job.phase === AcpJobPhases.REQUEST) { + console.log("New job request", job.id, memoToSign?.id, wallet); return await handleTaskRequest(job, memoToSign); } if (job.phase === AcpJobPhases.TRANSACTION) { + console.log("Job in transaction phase", job.id, memoToSign?.id, wallet); return await handleTaskTransaction(job, memoToSign); } @@ -98,8 +100,8 @@ const handleTaskRequest = async (job: AcpJob, memoToSign?: AcpMemo) => { } if (jobName === JobName.SWAP_TOKEN.toString()) { - await memoToSign.sign(true, "accepts swap token"); - return await job.createRequirementPayableMemo( + await memoToSign.sign(true, "accepts swap token"); + return await job.createRequirementPayableMemo( "Send me 1 USDC to swap to 1 USD", MemoType.PAYABLE_REQUEST, new FareAmount(1, config.baseFare), @@ -203,7 +205,7 @@ const handleTaskTransaction = async (job: AcpJob, memoToSign?: AcpMemo) => { }; async function main() { - const acpClient = new AcpClient({ + new AcpClient({ acpContractClient: await AcpContractClient.build( WHITELISTED_WALLET_PRIVATE_KEY, SELLER_ENTITY_ID, From 7b65c2f185de59376e2c722e1849f119e2f91059 Mon Sep 17 00:00:00 2001 From: Ang Weoy Yang Date: Fri, 19 Sep 2025 14:11:56 +0800 Subject: [PATCH 13/41] update v2 examples --- examples/acp-base/funds-v2/buyer.ts | 168 ++++++++++++++++----------- examples/acp-base/funds-v2/seller.ts | 2 +- 2 files changed, 101 insertions(+), 69 deletions(-) diff --git a/examples/acp-base/funds-v2/buyer.ts b/examples/acp-base/funds-v2/buyer.ts index 3883998..229912d 100644 --- a/examples/acp-base/funds-v2/buyer.ts +++ b/examples/acp-base/funds-v2/buyer.ts @@ -1,7 +1,7 @@ import * as readline from "readline"; import AcpClient, { AcpAgentSort, -AcpContractClient, + AcpContractClient, AcpGraduationStatus, AcpJob, AcpJobPhases, @@ -20,28 +20,50 @@ async function sleep(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); } +const SERVICE_REQUIREMENTS_JOB_TYPE_MAPPING: Record = { + open_position: { + symbol: "BTC", + amount: 0.001, + tp: { percentage: 5 }, + sl: { percentage: 2 }, + direction: "long", + }, + swap_token: { + fromSymbol: "BMW", + fromContractAddress: "0xbfAB80ccc15DF6fb7185f9498d6039317331846a", + amount: 0.01, + toSymbol: "USDC", + }, + close_partial_position: { + positionId: 0, + amount: 1, + }, + close_position: { positionId: 0 }, + close_job: "Close job and withdraw all", +} + async function main() { - let currentJob: AcpJob | null = null; - const config = baseSepoliaAcpConfig; + let currentJob: number | null = null; const acpClient = new AcpClient({ acpContractClient: await AcpContractClient.build( WHITELISTED_WALLET_PRIVATE_KEY, BUYER_ENTITY_ID, BUYER_AGENT_WALLET_ADDRESS, - config + baseSepoliaAcpConfig ), onNewTask: async (job: AcpJob, memoToSign?: AcpMemo) => { - console.log("New job", job.id, memoToSign?.id); + memoToSign && console.log("New job", job.id, memoToSign?.id); if (job.phase === AcpJobPhases.NEGOTIATION) { - console.log("Pay to job"); - await job.payAndAcceptRequirement("I accept the job requirements"); - currentJob = job; + console.log("Pay for job"); + await job.payAndAcceptRequirement(); + currentJob = job.id; return; } - currentJob = job; + currentJob = job.id; + console.log(job.phase) if (job.phase !== AcpJobPhases.TRANSACTION) { console.log("Job is not in transaction phase"); @@ -53,77 +75,87 @@ async function main() { return; } - // switch (memoToSign.payloadType) { - // case PayloadType.CLOSE_JOB_AND_WITHDRAW: - // await job.confirmJobClosure(memoToSign.id, true); - // console.log("Closed job"); - // break; - - // case PayloadType.RESPONSE_SWAP_TOKEN: - // await memoToSign.sign(true, "accepts swap token"); - // console.log("Swapped token"); - // break; - - // case PayloadType.CLOSE_POSITION: - // await job.confirmClosePosition(memoToSign.id, true); - // console.log("Closed position"); - // break; - - // default: - // console.log("Unhandled payload type", memoToSign.payloadType); - // } + switch (memoToSign.payloadType) { + case PayloadType.CLOSE_JOB_AND_WITHDRAW: + await job.confirmJobClosure(memoToSign.id, true); + console.log("Closed job"); + break; + + case PayloadType.RESPONSE_SWAP_TOKEN: + await memoToSign.sign(true, "accepts swap token"); + console.log("Swapped token"); + break; + + case PayloadType.CLOSE_POSITION: + await job.confirmClosePosition(memoToSign.id, true); + console.log("Closed position"); + break; + + default: + console.log("Unhandled payload type", memoToSign.payloadType); + } }, }); console.log("Initiating job"); const agents = await acpClient.browseAgents( - "calm_seller", - { + "", + { sort_by: [AcpAgentSort.SUCCESSFUL_JOB_COUNT], top_k: 5, graduationStatus: AcpGraduationStatus.ALL, onlineStatus: AcpOnlineStatus.ALL, - } - ); + } + ); console.log(agents); - console.log(agents[0].jobOfferings); - - const jobId = await agents[0].jobOfferings[0].initiateJob({ - "fromSymbol": "USDC", - "fromContractAddress": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", // USDC token address - "amount": 0.01, - "toSymbol": "BMW", - "toContractAddress": "0xbfAB80ccc15DF6fb7185f9498d6039317331846a" // BMW token address + const { jobOfferings } = agents[0]; + console.log(jobOfferings); + const actionsDefinition = (jobOfferings ?? []) + .map((offering, idx) => { + return { + index: idx + 1, + desc: offering.name, + action: async() => { + currentJob = await offering.initiateJob(SERVICE_REQUIREMENTS_JOB_TYPE_MAPPING[offering.name]) + }, + }; + }) + + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, }); - console.log("Job initiated", jobId); - - // while (true) { - // await sleep(5000); - - // if (!currentJob) { - // console.log("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("Select an action (enter the number): "); - // 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."); - // } - // } + + const question = (prompt: string): Promise => + new Promise((resolve) => rl.question(prompt, resolve)); + + while (true) { + await sleep(5000); + + if (currentJob) { + // 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("Select an action (enter the number): "); + 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/seller.ts b/examples/acp-base/funds-v2/seller.ts index e8e20a7..5775dcc 100644 --- a/examples/acp-base/funds-v2/seller.ts +++ b/examples/acp-base/funds-v2/seller.ts @@ -102,7 +102,7 @@ const handleTaskRequest = async (job: AcpJob, memoToSign?: AcpMemo) => { if (jobName === JobName.SWAP_TOKEN.toString()) { await memoToSign.sign(true, "accepts swap token"); return await job.createRequirementPayableMemo( - "Send me 1 USDC to swap to 1 USD", + "Send me 1 USDC to swap to 1 VIRTUAL", MemoType.PAYABLE_REQUEST, new FareAmount(1, config.baseFare), job.providerAddress From e2bd39835cae2dcfab4d833a04c4b9613ac9b05f Mon Sep 17 00:00:00 2001 From: jxx016 Date: Sun, 21 Sep 2025 12:41:29 -0500 Subject: [PATCH 14/41] adding v2 examples for self and external evaluation --- .../external-evaluation-v2/.env.example | 9 ++ .../acp-base/external-evaluation-v2/README.md | 86 +++++++++++ .../acp-base/external-evaluation-v2/buyer.ts | 69 +++++++++ .../acp-base/external-evaluation-v2/env.ts | 44 ++++++ .../external-evaluation-v2/evaluator.ts | 36 +++++ .../acp-base/external-evaluation-v2/seller.ts | 49 ++++++ .../acp-base/self-evaluation-v2/README.md | 145 ++++++++++++++++++ examples/acp-base/self-evaluation-v2/buyer.ts | 75 +++++++++ examples/acp-base/self-evaluation-v2/env.ts | 37 +++++ .../acp-base/self-evaluation-v2/seller.ts | 49 ++++++ 10 files changed, 599 insertions(+) create mode 100644 examples/acp-base/external-evaluation-v2/.env.example create mode 100644 examples/acp-base/external-evaluation-v2/README.md create mode 100644 examples/acp-base/external-evaluation-v2/buyer.ts create mode 100644 examples/acp-base/external-evaluation-v2/env.ts create mode 100644 examples/acp-base/external-evaluation-v2/evaluator.ts create mode 100644 examples/acp-base/external-evaluation-v2/seller.ts create mode 100644 examples/acp-base/self-evaluation-v2/README.md create mode 100644 examples/acp-base/self-evaluation-v2/buyer.ts create mode 100644 examples/acp-base/self-evaluation-v2/env.ts create mode 100644 examples/acp-base/self-evaluation-v2/seller.ts 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..1313f0a --- /dev/null +++ b/examples/acp-base/external-evaluation-v2/README.md @@ -0,0 +1,86 @@ +# 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..ecd9981 --- /dev/null +++ b/examples/acp-base/external-evaluation-v2/buyer.ts @@ -0,0 +1,69 @@ +import AcpClient, { + AcpContractClient, + AcpJobPhases, + AcpJob, + AcpMemo, + AcpAgentSort, + AcpGraduationStatus, + AcpOnlineStatus, + baseSepoliaAcpConfig +} from "../../../src"; +import { + BUYER_AGENT_WALLET_ADDRESS, + WHITELISTED_WALLET_PRIVATE_KEY, + BUYER_ENTITY_ID, + EVALUATOR_AGENT_WALLET_ADDRESS +} from "./env"; + +async function buyer() { + const config = baseSepoliaAcpConfig; + + const acpClient = new AcpClient({ + acpContractClient: await AcpContractClient.build( + WHITELISTED_WALLET_PRIVATE_KEY, + BUYER_ENTITY_ID, + BUYER_AGENT_WALLET_ADDRESS, + config // v2 requires config parameter + ), + onNewTask: async (job: AcpJob, memoToSign?: AcpMemo) => { // v2 has memoToSign parameter + if ( + job.phase === AcpJobPhases.NEGOTIATION && + job.memos.find((m) => m.nextPhase === AcpJobPhases.TRANSACTION) + ) { + console.log("Paying job", job); + await job.pay(job.price); + console.log(`Job ${job.id} paid`); + } else if (job.phase === AcpJobPhases.COMPLETED) { + console.log(`Job ${job.id} completed`); + } 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.", // v2 simplified - uses string instead of schema object + 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..a00b9c7 --- /dev/null +++ b/examples/acp-base/external-evaluation-v2/evaluator.ts @@ -0,0 +1,36 @@ +import AcpClient, { + AcpContractClient, + AcpJob, + baseSepoliaAcpConfig +} from '../../../src'; +import { + EVALUATOR_AGENT_WALLET_ADDRESS, + EVALUATOR_ENTITY_ID, + WHITELISTED_WALLET_PRIVATE_KEY +} from "./env"; + +async function evaluator() { + const config = baseSepoliaAcpConfig; + + const acpClient = new AcpClient({ + acpContractClient: await AcpContractClient.build( + WHITELISTED_WALLET_PRIVATE_KEY, + EVALUATOR_ENTITY_ID, + EVALUATOR_AGENT_WALLET_ADDRESS, + config // v2 requires config parameter + ), + 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..83bff1c --- /dev/null +++ b/examples/acp-base/external-evaluation-v2/seller.ts @@ -0,0 +1,49 @@ +import AcpClient, { + AcpContractClient, + AcpJobPhases, + AcpJob, + AcpMemo, + baseSepoliaAcpConfig +} from '../../../src'; +import { + SELLER_AGENT_WALLET_ADDRESS, + SELLER_ENTITY_ID, + WHITELISTED_WALLET_PRIVATE_KEY +} from "./env"; + +async function seller() { + const config = baseSepoliaAcpConfig; + + const acpClient = new AcpClient({ + acpContractClient: await AcpContractClient.build( + WHITELISTED_WALLET_PRIVATE_KEY, + SELLER_ENTITY_ID, + SELLER_AGENT_WALLET_ADDRESS, + config // v2 requires config parameter + ), + onNewTask: async (job: AcpJob, memoToSign?: AcpMemo) => { // v2 has memoToSign parameter + if ( + job.phase === AcpJobPhases.REQUEST && + job.memos.find((m) => m.nextPhase === AcpJobPhases.NEGOTIATION) + ) { + console.log("Responding to job", job); + await job.respond(true); + console.log(`Job ${job.id} responded`); + } else if ( + job.phase === AcpJobPhases.TRANSACTION && + job.memos.find((m) => m.nextPhase === AcpJobPhases.EVALUATION) + ) { + console.log("Delivering job", job); + await job.deliver( + { + type: "url", + value: "https://example.com", + } + ); + console.log(`Job ${job.id} delivered`); + } + }, + }); +} + +seller(); 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..b575b02 --- /dev/null +++ b/examples/acp-base/self-evaluation-v2/README.md @@ -0,0 +1,145 @@ +# 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..fe5b7dd --- /dev/null +++ b/examples/acp-base/self-evaluation-v2/buyer.ts @@ -0,0 +1,75 @@ +import AcpClient, { + AcpContractClient, + AcpJobPhases, + AcpJob, + AcpMemo, + AcpAgentSort, + AcpGraduationStatus, + AcpOnlineStatus, + baseSepoliaAcpConfig +} from "../../../src"; +import { + BUYER_AGENT_WALLET_ADDRESS, + WHITELISTED_WALLET_PRIVATE_KEY, + BUYER_ENTITY_ID, +} from "./env"; + +async function buyer() { + const config = baseSepoliaAcpConfig; + + const acpClient = new AcpClient({ + acpContractClient: await AcpContractClient.build( + WHITELISTED_WALLET_PRIVATE_KEY, + BUYER_ENTITY_ID, + BUYER_AGENT_WALLET_ADDRESS, + config // v2 requires config parameter + ), + onNewTask: async (job: AcpJob, memoToSign?: AcpMemo) => { // v2 has memoToSign parameter + if ( + job.phase === AcpJobPhases.NEGOTIATION && + job.memos.find((m) => m.nextPhase === AcpJobPhases.TRANSACTION) + ) { + console.log("Paying job", job); + await job.pay(job.price); + console.log(`Job ${job.id} paid`); + } else if (job.phase === AcpJobPhases.COMPLETED) { + console.log(`Job ${job.id} completed`); + } else if (job.phase === AcpJobPhases.REJECTED) { + console.log(`Job ${job.id} rejected`); + } + }, + onEvaluate: async (job: AcpJob) => { + console.log("Evaluation function called", job); + await job.evaluate(true, "Self-evaluated and approved"); + console.log(`Job ${job.id} evaluated`); + }, + }); + + // Browse available agents based on a keyword + const relevantAgents = await acpClient.browseAgents( + "", // v2 example uses fund_provider + { + 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( + { "": "" }, // Use object to match schema requirement + 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..83bff1c --- /dev/null +++ b/examples/acp-base/self-evaluation-v2/seller.ts @@ -0,0 +1,49 @@ +import AcpClient, { + AcpContractClient, + AcpJobPhases, + AcpJob, + AcpMemo, + baseSepoliaAcpConfig +} from '../../../src'; +import { + SELLER_AGENT_WALLET_ADDRESS, + SELLER_ENTITY_ID, + WHITELISTED_WALLET_PRIVATE_KEY +} from "./env"; + +async function seller() { + const config = baseSepoliaAcpConfig; + + const acpClient = new AcpClient({ + acpContractClient: await AcpContractClient.build( + WHITELISTED_WALLET_PRIVATE_KEY, + SELLER_ENTITY_ID, + SELLER_AGENT_WALLET_ADDRESS, + config // v2 requires config parameter + ), + onNewTask: async (job: AcpJob, memoToSign?: AcpMemo) => { // v2 has memoToSign parameter + if ( + job.phase === AcpJobPhases.REQUEST && + job.memos.find((m) => m.nextPhase === AcpJobPhases.NEGOTIATION) + ) { + console.log("Responding to job", job); + await job.respond(true); + console.log(`Job ${job.id} responded`); + } else if ( + job.phase === AcpJobPhases.TRANSACTION && + job.memos.find((m) => m.nextPhase === AcpJobPhases.EVALUATION) + ) { + console.log("Delivering job", job); + await job.deliver( + { + type: "url", + value: "https://example.com", + } + ); + console.log(`Job ${job.id} delivered`); + } + }, + }); +} + +seller(); From 359d8e7554c964ce3b5b4a996db8fd1fea0fc2af Mon Sep 17 00:00:00 2001 From: Ang Weoy Yang Date: Tue, 23 Sep 2025 12:46:03 +0800 Subject: [PATCH 15/41] docs: update funds-v2 examples with more logging --- examples/acp-base/funds-v2/buyer.ts | 22 ++++++++++++++-------- examples/acp-base/funds-v2/seller.ts | 16 ++++++++++------ 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/examples/acp-base/funds-v2/buyer.ts b/examples/acp-base/funds-v2/buyer.ts index 229912d..a95561f 100644 --- a/examples/acp-base/funds-v2/buyer.ts +++ b/examples/acp-base/funds-v2/buyer.ts @@ -29,17 +29,17 @@ const SERVICE_REQUIREMENTS_JOB_TYPE_MAPPING: Record = { direction: "long", }, swap_token: { - fromSymbol: "BMW", - fromContractAddress: "0xbfAB80ccc15DF6fb7185f9498d6039317331846a", + fromSymbol: "USDC", + fromContractAddress: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", amount: 0.01, - toSymbol: "USDC", - }, - close_partial_position: { - positionId: 0, - amount: 1, + toSymbol: "BMW", }, close_position: { positionId: 0 }, - close_job: "Close job and withdraw all", + withdraw: { + symbol: "USDC", + contractAddress: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + amount: 0.01, + }, } async function main() { @@ -95,6 +95,12 @@ async function main() { console.log("Unhandled payload type", memoToSign.payloadType); } }, + onEvaluate: async (job: AcpJob) => { + console.log("Evaluation function called", job); + await job.evaluate(true, "job auto-evaluated") + console.log(`Job ${job.id} evaluated`); + currentJob = null + } }); console.log("Initiating job"); diff --git a/examples/acp-base/funds-v2/seller.ts b/examples/acp-base/funds-v2/seller.ts index 5775dcc..ae926d6 100644 --- a/examples/acp-base/funds-v2/seller.ts +++ b/examples/acp-base/funds-v2/seller.ts @@ -86,35 +86,39 @@ const handleTaskRequest = async (job: AcpJob, memoToSign?: AcpMemo) => { if (jobName === JobName.OPEN_POSITION.toString()) { await memoToSign.sign(true, "accepts open position"); + console.log("Accepts open position request"); return await job.createRequirementPayableMemo( "Send me 1 USDC to open position", MemoType.PAYABLE_REQUEST, - new FareAmount(1, config.baseFare), + new FareAmount((job.requirement as Record)?.amount, config.baseFare), job.providerAddress ); } if (jobName === JobName.CLOSE_POSITION.toString()) { await memoToSign.sign(true, "accepts close position"); + console.log("Accepts close position request"); return await job.createRequirementMemo("Closing a random position"); } if (jobName === JobName.SWAP_TOKEN.toString()) { await memoToSign.sign(true, "accepts swap token"); + console.log("Accepts swap token request"); return await job.createRequirementPayableMemo( "Send me 1 USDC to swap to 1 VIRTUAL", MemoType.PAYABLE_REQUEST, - new FareAmount(1, config.baseFare), + new FareAmount((job.requirement as Record)?.amount, config.baseFare), job.providerAddress ); } if (jobName === JobName.WITHDRAW.toString()) { await memoToSign.sign(true, "accepts withdraw"); + console.log("Accepts withdrawal request"); return await job.createRequirementPayableMemo( "Withdrawing a random amount", MemoType.PAYABLE_TRANSFER_ESCROW, - new FareAmount(1, config.baseFare), + new FareAmount((job.requirement as Record)?.amount, config.baseFare), job.providerAddress ); } @@ -132,9 +136,9 @@ const handleTaskTransaction = async (job: AcpJob, memoToSign?: AcpMemo) => { if (jobName === JobName.OPEN_POSITION.toString()) { const wallet = getClientWallet(job.clientAddress); - + const position = wallet.positions.find((p) => p.symbol === "USDC"); - + if (position) { position.amount += 1; } else { @@ -155,7 +159,7 @@ const handleTaskTransaction = async (job: AcpJob, memoToSign?: AcpMemo) => { const wallet = getClientWallet(job.clientAddress); const position = wallet.positions.find((p) => p.symbol === "USDC"); wallet.positions = wallet.positions.filter((p) => p.symbol !== "USDC"); - + const asset = wallet.assets.find( (a) => a.fare.contractAddress === config.baseFare.contractAddress ); From e032fcbf6f0856f39eda849480cbcf2ac6776001 Mon Sep 17 00:00:00 2001 From: Ang Weoy Yang Date: Tue, 23 Sep 2025 13:30:13 +0800 Subject: [PATCH 16/41] refactor: remove withdrawal examples --- examples/acp-base/funds-v2/buyer.ts | 17 ++++++----------- examples/acp-base/funds-v2/seller.ts | 20 -------------------- 2 files changed, 6 insertions(+), 31 deletions(-) diff --git a/examples/acp-base/funds-v2/buyer.ts b/examples/acp-base/funds-v2/buyer.ts index a95561f..f96ed14 100644 --- a/examples/acp-base/funds-v2/buyer.ts +++ b/examples/acp-base/funds-v2/buyer.ts @@ -21,6 +21,12 @@ async function sleep(ms: number) { } const SERVICE_REQUIREMENTS_JOB_TYPE_MAPPING: Record = { + swap_token: { + fromSymbol: "USDC", + fromContractAddress: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + amount: 0.01, + toSymbol: "BMW", + }, open_position: { symbol: "BTC", amount: 0.001, @@ -28,18 +34,7 @@ const SERVICE_REQUIREMENTS_JOB_TYPE_MAPPING: Record = { sl: { percentage: 2 }, direction: "long", }, - swap_token: { - fromSymbol: "USDC", - fromContractAddress: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", - amount: 0.01, - toSymbol: "BMW", - }, close_position: { positionId: 0 }, - withdraw: { - symbol: "USDC", - contractAddress: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", - amount: 0.01, - }, } async function main() { diff --git a/examples/acp-base/funds-v2/seller.ts b/examples/acp-base/funds-v2/seller.ts index ae926d6..e07e096 100644 --- a/examples/acp-base/funds-v2/seller.ts +++ b/examples/acp-base/funds-v2/seller.ts @@ -24,7 +24,6 @@ enum JobName { OPEN_POSITION = "open_position", CLOSE_POSITION = "close_position", SWAP_TOKEN = "swap_token", - WITHDRAW = "withdraw", } interface IPosition { @@ -112,17 +111,6 @@ const handleTaskRequest = async (job: AcpJob, memoToSign?: AcpMemo) => { ); } - if (jobName === JobName.WITHDRAW.toString()) { - await memoToSign.sign(true, "accepts withdraw"); - console.log("Accepts withdrawal request"); - return await job.createRequirementPayableMemo( - "Withdrawing a random amount", - MemoType.PAYABLE_TRANSFER_ESCROW, - new FareAmount((job.requirement as Record)?.amount, config.baseFare), - job.providerAddress - ); - } - console.error("Job name not supported", jobName); return; }; @@ -196,14 +184,6 @@ const handleTaskTransaction = async (job: AcpJob, memoToSign?: AcpMemo) => { return; } - if (jobName === JobName.WITHDRAW.toString()) { - await job.deliver({ - type: "message", - value: "Withdrawn amount with hash 0x1234567890", - }); - return; - } - console.error("Job name not supported", jobName); return; }; From 80bccab7399629a9e3bb9875060b5d10dbec7cc7 Mon Sep 17 00:00:00 2001 From: Ang Weoy Yang Date: Tue, 23 Sep 2025 16:02:41 +0800 Subject: [PATCH 17/41] feat: add job rejection abstraction --- src/acpClient.ts | 13 ++++++++++++- src/acpJob.ts | 7 +++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/acpClient.ts b/src/acpClient.ts index 2b1f049..fd8d556 100644 --- a/src/acpClient.ts +++ b/src/acpClient.ts @@ -24,10 +24,11 @@ import AcpError from "./acpError"; import { ethFare, FareAmount, - FareBigInt, FareAmountBase, + FareBigInt, wethFare, } from "./acpFare"; + const { version } = require("../package.json"); enum SocketEvents { @@ -473,6 +474,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, diff --git a/src/acpJob.ts b/src/acpJob.ts index f25447f..0e10bc2 100644 --- a/src/acpJob.ts +++ b/src/acpJob.ts @@ -196,6 +196,13 @@ class AcpJob { ); } + async reject(reason?: string) { + return await this.acpClient.rejectJob( + this.id, + `Job ${this.id} rejected. ${reason || ''}`, + ) + } + async deliver(deliverable: IDeliverable) { if (this.latestMemo?.nextPhase !== AcpJobPhases.EVALUATION) { throw new AcpError("No transaction memo found"); From 45d4dc3ff800b871fbe1c2d2c9d673307c00b371 Mon Sep 17 00:00:00 2001 From: Zuhwa Date: Wed, 24 Sep 2025 22:18:03 +0800 Subject: [PATCH 18/41] approve allowance during transfer --- src/acpClient.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/acpClient.ts b/src/acpClient.ts index fd8d556..c2b4212 100644 --- a/src/acpClient.ts +++ b/src/acpClient.ts @@ -296,6 +296,13 @@ class AcpClient { 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( From ecf60e98dbf998154fbf651a6babc8850eedc930 Mon Sep 17 00:00:00 2001 From: Ang Weoy Yang Date: Thu, 25 Sep 2025 00:18:19 +0800 Subject: [PATCH 19/41] feat: rename acp config file --- src/{AcpConfigs.ts => acpConfigs.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/{AcpConfigs.ts => acpConfigs.ts} (100%) diff --git a/src/AcpConfigs.ts b/src/acpConfigs.ts similarity index 100% rename from src/AcpConfigs.ts rename to src/acpConfigs.ts From 519d993435c0f1d0e72d8034530122502c8830de Mon Sep 17 00:00:00 2001 From: Ang Weoy Yang Date: Tue, 23 Sep 2025 16:09:35 +0800 Subject: [PATCH 20/41] docs: improve example code --- .../acp-base/external-evaluation-v2/README.md | 2 - .../acp-base/external-evaluation-v2/buyer.ts | 19 +- .../external-evaluation-v2/evaluator.ts | 6 +- .../acp-base/external-evaluation-v2/seller.ts | 17 +- examples/acp-base/funds-v2/README.md | 6 - examples/acp-base/funds-v2/buyer.ts | 111 ++++----- examples/acp-base/funds-v2/jobTypes.ts | 29 +++ examples/acp-base/funds-v2/seller.ts | 229 +++++++++--------- examples/acp-base/polling-mode/buyer.ts | 2 +- .../acp-base/self-evaluation-v2/README.md | 1 - examples/acp-base/self-evaluation-v2/buyer.ts | 21 +- .../acp-base/self-evaluation-v2/seller.ts | 23 +- 12 files changed, 257 insertions(+), 209 deletions(-) create mode 100644 examples/acp-base/funds-v2/jobTypes.ts diff --git a/examples/acp-base/external-evaluation-v2/README.md b/examples/acp-base/external-evaluation-v2/README.md index 1313f0a..b976790 100644 --- a/examples/acp-base/external-evaluation-v2/README.md +++ b/examples/acp-base/external-evaluation-v2/README.md @@ -82,5 +82,3 @@ npx ts-node buyer.ts 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 index ecd9981..01d17f9 100644 --- a/examples/acp-base/external-evaluation-v2/buyer.ts +++ b/examples/acp-base/external-evaluation-v2/buyer.ts @@ -16,23 +16,28 @@ import { } from "./env"; async function buyer() { - const config = baseSepoliaAcpConfig; - const acpClient = new AcpClient({ acpContractClient: await AcpContractClient.build( WHITELISTED_WALLET_PRIVATE_KEY, BUYER_ENTITY_ID, BUYER_AGENT_WALLET_ADDRESS, - config // v2 requires config parameter + baseSepoliaAcpConfig ), - onNewTask: async (job: AcpJob, memoToSign?: AcpMemo) => { // v2 has memoToSign parameter + onNewTask: async (job: AcpJob, memoToSign?: AcpMemo) => { if ( job.phase === AcpJobPhases.NEGOTIATION && - job.memos.find((m) => m.nextPhase === AcpJobPhases.TRANSACTION) + memoToSign?.nextPhase === AcpJobPhases.TRANSACTION ) { console.log("Paying job", job); - await job.pay(job.price); + 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); + 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`); } else if (job.phase === AcpJobPhases.REJECTED) { @@ -58,7 +63,7 @@ async function buyer() { const chosenJobOffering = chosenAgent.jobOfferings[0]; // v2 uses jobOfferings instead of offerings const jobId = await chosenJobOffering.initiateJob( - "Help me to generate a flower meme.", // v2 simplified - uses string instead of schema object + "Help me to generate a flower meme.", EVALUATOR_AGENT_WALLET_ADDRESS, // Use external evaluator address new Date(Date.now() + 1000 * 60 * 60 * 24) // expiredAt ); diff --git a/examples/acp-base/external-evaluation-v2/evaluator.ts b/examples/acp-base/external-evaluation-v2/evaluator.ts index a00b9c7..3a655b8 100644 --- a/examples/acp-base/external-evaluation-v2/evaluator.ts +++ b/examples/acp-base/external-evaluation-v2/evaluator.ts @@ -10,14 +10,12 @@ import { } from "./env"; async function evaluator() { - const config = baseSepoliaAcpConfig; - - const acpClient = new AcpClient({ + new AcpClient({ acpContractClient: await AcpContractClient.build( WHITELISTED_WALLET_PRIVATE_KEY, EVALUATOR_ENTITY_ID, EVALUATOR_AGENT_WALLET_ADDRESS, - config // v2 requires config parameter + baseSepoliaAcpConfig ), onEvaluate: async (job: AcpJob) => { console.log("[onEvaluate] Evaluation function called", job.memos); diff --git a/examples/acp-base/external-evaluation-v2/seller.ts b/examples/acp-base/external-evaluation-v2/seller.ts index 83bff1c..7e0b7e7 100644 --- a/examples/acp-base/external-evaluation-v2/seller.ts +++ b/examples/acp-base/external-evaluation-v2/seller.ts @@ -12,16 +12,14 @@ import { } from "./env"; async function seller() { - const config = baseSepoliaAcpConfig; - - const acpClient = new AcpClient({ + new AcpClient({ acpContractClient: await AcpContractClient.build( WHITELISTED_WALLET_PRIVATE_KEY, SELLER_ENTITY_ID, SELLER_AGENT_WALLET_ADDRESS, - config // v2 requires config parameter + baseSepoliaAcpConfig ), - onNewTask: async (job: AcpJob, memoToSign?: AcpMemo) => { // v2 has memoToSign parameter + onNewTask: async (job: AcpJob, memoToSign?: AcpMemo) => { if ( job.phase === AcpJobPhases.REQUEST && job.memos.find((m) => m.nextPhase === AcpJobPhases.NEGOTIATION) @@ -31,8 +29,13 @@ async function seller() { console.log(`Job ${job.id} responded`); } else if ( job.phase === AcpJobPhases.TRANSACTION && - job.memos.find((m) => m.nextPhase === AcpJobPhases.EVALUATION) + memoToSign?.nextPhase === AcpJobPhases.EVALUATION ) { + // // to cater cases where agent decide to reject job after payment has been made + // console.log("Rejecting job", job) + // await job.reject("Job requirement does not meet agent capability"); + // console.log(`Job ${job.id} rejected`); + console.log("Delivering job", job); await job.deliver( { @@ -42,7 +45,7 @@ async function seller() { ); console.log(`Job ${job.id} delivered`); } - }, + } }); } diff --git a/examples/acp-base/funds-v2/README.md b/examples/acp-base/funds-v2/README.md index 6bbdc5e..6052895 100644 --- a/examples/acp-base/funds-v2/README.md +++ b/examples/acp-base/funds-v2/README.md @@ -8,7 +8,6 @@ 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 -- **Withdrawal Operations**: Custom withdrawal job implementations ## Files @@ -18,7 +17,6 @@ The buyer agent demonstrates how to: - **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 -- **Withdraw Funds**: Request fund withdrawals - **Interactive CLI**: Provides a command-line interface for real-time interaction **Key Features:** @@ -39,7 +37,6 @@ The seller agent demonstrates how to: - `OPEN_POSITION`: Create new trading positions - `CLOSE_POSITION`: Close existing positions - `SWAP_TOKEN`: Perform token swaps -- `WITHDRAW`: Process withdrawal requests ## Setup @@ -82,7 +79,6 @@ npx ts-node buyer.ts - Open trading positions with TP/SL parameters - Close existing positions - Swap tokens (e.g., USDC to USD) - - Withdraw funds from positions - Close the entire job 3. **Interactive Operations**: Use the CLI menu to test different scenarios: @@ -91,8 +87,6 @@ npx ts-node buyer.ts 1. Open position 2. Close position 3. Swap token - 4. Withdraw - 5. Close job ``` 4. **Payment Handling**: The system automatically handles: diff --git a/examples/acp-base/funds-v2/buyer.ts b/examples/acp-base/funds-v2/buyer.ts index f96ed14..dfe3da2 100644 --- a/examples/acp-base/funds-v2/buyer.ts +++ b/examples/acp-base/funds-v2/buyer.ts @@ -8,37 +8,39 @@ import AcpClient, { AcpMemo, AcpOnlineStatus, baseSepoliaAcpConfig, - PayloadType, + MemoType, } from "../../../src"; import { BUYER_AGENT_WALLET_ADDRESS, - WHITELISTED_WALLET_PRIVATE_KEY, 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 SERVICE_REQUIREMENTS_JOB_TYPE_MAPPING: Record = { +const SERVICE_REQUIREMENTS_JOB_TYPE_MAPPING: Record = { swap_token: { fromSymbol: "USDC", - fromContractAddress: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", - amount: 0.01, + fromContractAddress: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", // Base Sepolia USDC Token + amount: 0.08, toSymbol: "BMW", + toContractAddress: "0xbfAB80ccc15DF6fb7185f9498d6039317331846a" }, open_position: { symbol: "BTC", - amount: 0.001, + amount: 0.09, tp: { percentage: 5 }, sl: { percentage: 2 }, direction: "long", }, - close_position: { positionId: 0 }, + close_position: { symbol: "BTC" }, } async function main() { - let currentJob: number | null = null; + let currentJobId: number | null = null; const acpClient = new AcpClient({ acpContractClient: await AcpContractClient.build( @@ -48,58 +50,58 @@ async function main() { baseSepoliaAcpConfig ), onNewTask: async (job: AcpJob, memoToSign?: AcpMemo) => { - memoToSign && console.log("New job", job.id, memoToSign?.id); - - if (job.phase === AcpJobPhases.NEGOTIATION) { - console.log("Pay for job"); - await job.payAndAcceptRequirement(); - currentJob = job.id; - return; - } - - currentJob = job.id; - console.log(job.phase) - - if (job.phase !== AcpJobPhases.TRANSACTION) { - console.log("Job is not in transaction phase"); - return; - } - + const { id: jobId, phase: jobPhase } = job; if (!memoToSign) { - console.log("No memo to sign"); + console.log("[onNewTask] No memo to sign", { jobId }); + if (job.phase === AcpJobPhases.REJECTED) { + currentJobId = null; + } return; } - - switch (memoToSign.payloadType) { - case PayloadType.CLOSE_JOB_AND_WITHDRAW: - await job.confirmJobClosure(memoToSign.id, true); - console.log("Closed job"); - break; - - case PayloadType.RESPONSE_SWAP_TOKEN: - await memoToSign.sign(true, "accepts swap token"); - console.log("Swapped token"); - break; - - case PayloadType.CLOSE_POSITION: - await job.confirmClosePosition(memoToSign.id, true); - console.log("Closed position"); - break; - - default: - console.log("Unhandled payload type", memoToSign.payloadType); + 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 job", jobId); + await job.payAndAcceptRequirement(); + currentJobId = jobId; + console.log("[onNewTask] Job paid", jobId); + } else if ( + jobPhase === AcpJobPhases.TRANSACTION + ) { + if (memoToSign.nextPhase === AcpJobPhases.REJECTED) { + console.log("[onNewTask] Signing job rejection memo", { jobId, memoId }); + await memoToSign.sign(true, "Accepted 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, "Accepted funds transfer"); + console.log("[onNewTask] Funds transfer memo signed", { jobId }); + } } }, onEvaluate: async (job: AcpJob) => { - console.log("Evaluation function called", job); - await job.evaluate(true, "job auto-evaluated") - console.log(`Job ${job.id} evaluated`); - currentJob = null + console.log( + "[onEvaluate] Evaluation function called", + { + jobId: job.id, + requirement: job.requirement, + deliverable: job.deliverable, + } + ); + await job.evaluate(true, "job auto-evaluated"); + console.log(`[onEvaluate] Job ${job.id} evaluated`); + currentJobId = null; } }); - console.log("Initiating job"); - const agents = await acpClient.browseAgents( "", { @@ -118,7 +120,7 @@ async function main() { index: idx + 1, desc: offering.name, action: async() => { - currentJob = await offering.initiateJob(SERVICE_REQUIREMENTS_JOB_TYPE_MAPPING[offering.name]) + currentJobId = await offering.initiateJob(SERVICE_REQUIREMENTS_JOB_TYPE_MAPPING[offering.name]) }, }; }) @@ -134,7 +136,7 @@ async function main() { while (true) { await sleep(5000); - if (currentJob) { + if (currentJobId) { // No job found, waiting for new job continue; } @@ -144,7 +146,8 @@ async function main() { console.log(`${action.index}. ${action.desc}`); }); - const answer = await question("Select an action (enter the number): "); + const answer = await question("\nSelect an action (enter the number): "); + console.log("Initiating job..."); const selectedIndex = parseInt(answer, 10); const selectedAction = actionsDefinition.find( diff --git a/examples/acp-base/funds-v2/jobTypes.ts b/examples/acp-base/funds-v2/jobTypes.ts new file mode 100644 index 0000000..bb6f106 --- /dev/null +++ b/examples/acp-base/funds-v2/jobTypes.ts @@ -0,0 +1,29 @@ +export type V2DemoSwapTokenPayload = { + fromSymbol: string; + fromContractAddress: `0x${string}`; + amount: number; + toSymbol: string; + toContractAddress: `0x${string}`; +} + +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/seller.ts b/examples/acp-base/funds-v2/seller.ts index e07e096..4abdbb0 100644 --- a/examples/acp-base/funds-v2/seller.ts +++ b/examples/acp-base/funds-v2/seller.ts @@ -5,6 +5,7 @@ import AcpClient, { AcpJobPhases, AcpMemo, baseSepoliaAcpConfig, + Fare, FareAmount, MemoType, } from "../../../src"; @@ -15,6 +16,11 @@ import { SELLER_ENTITY_ID, WHITELISTED_WALLET_PRIVATE_KEY } from "./env"; +import { + V2DemoClosePositionPayload, + V2DemoOpenPositionPayload, + V2DemoSwapTokenPayload +} from "./jobTypes"; dotenv.config(); @@ -55,138 +61,143 @@ const getClientWallet = (address: Address): IClientWallet => { }; const onNewTask = async (job: AcpJob, memoToSign?: AcpMemo) => { - const wallet = getClientWallet(job.clientAddress); - - if (job.phase === AcpJobPhases.REQUEST) { - console.log("New job request", job.id, memoToSign?.id, wallet); - return await handleTaskRequest(job, memoToSign); + const { id: jobId, phase: jobPhase, name: jobName } = job; + if (!memoToSign) { + console.log("[onNewTask] No memo to sign", { jobId }); + return; } + const memoId = memoToSign.id; - if (job.phase === AcpJobPhases.TRANSACTION) { - console.log("Job in transaction phase", job.id, memoToSign?.id, wallet); - return await handleTaskTransaction(job, memoToSign); - } + console.info("[onNewTask] Received job", { jobId, phase: AcpJobPhases[jobPhase], jobName, memoId }); - console.error("Job is not in request or transaction phase", job.phase); - return; + 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) => { - if (!memoToSign) { - console.error("Memo to sign not found", memoToSign); - return; - } + const { id: jobId, name: jobName } = job; + const memoId = memoToSign?.id; - const jobName = job.name; - if (!jobName) { - console.error("job name not found", job); + if (!memoToSign || !jobName) { + console.error("[handleTaskRequest] Missing data", { jobId, memoId, jobName }); return; } - if (jobName === JobName.OPEN_POSITION.toString()) { - await memoToSign.sign(true, "accepts open position"); - console.log("Accepts open position request"); - return await job.createRequirementPayableMemo( - "Send me 1 USDC to open position", - MemoType.PAYABLE_REQUEST, - new FareAmount((job.requirement as Record)?.amount, config.baseFare), - job.providerAddress - ); - } + switch (jobName) { + case JobName.OPEN_POSITION: + console.log("Accepts position opening request", job.requirement); + await memoToSign.sign(true, "Accepts position opening"); + return job.createRequirementPayableMemo( + "Send me USDC to open position", + MemoType.PAYABLE_REQUEST, + new FareAmount( + Number((job.requirement as V2DemoOpenPositionPayload)?.amount), + config.baseFare // Open position against ACP Base Currency: USDC + ), + job.providerAddress + ); - if (jobName === JobName.CLOSE_POSITION.toString()) { - await memoToSign.sign(true, "accepts close position"); - console.log("Accepts close position request"); - return await job.createRequirementMemo("Closing a random position"); - } + case JobName.CLOSE_POSITION: + const wallet = getClientWallet(job.clientAddress); + const symbol = (job.requirement as V2DemoClosePositionPayload)?.symbol + const position = wallet.positions.find((p) => p.symbol === symbol); + const positionIsValid = !!position && position.amount > 0 + console.log(`${positionIsValid ? "Accepts" : "Rejects"} position closing request`, job.requirement); + await memoToSign.sign(positionIsValid, `${positionIsValid ? "Accepts" : "Rejects"} position closing`); + if (positionIsValid) { + return job.createRequirementMemo(`Close ${symbol} position as per requested.`); + } + break; + + case JobName.SWAP_TOKEN: + console.log("Accepts token swapping request", job.requirement); + await memoToSign.sign(true, "Accepts token swapping request"); + return job.createRequirementPayableMemo( + "Send me USDC to swap to VIRTUAL", + MemoType.PAYABLE_REQUEST, + new FareAmount( + Number((job.requirement as V2DemoSwapTokenPayload)?.amount), + await Fare.fromContractAddress( // Constructing Fare for the token to swap from + (job.requirement as V2DemoSwapTokenPayload)?.fromContractAddress, + baseSepoliaAcpConfig + ) + ), + job.providerAddress + ); - if (jobName === JobName.SWAP_TOKEN.toString()) { - await memoToSign.sign(true, "accepts swap token"); - console.log("Accepts swap token request"); - return await job.createRequirementPayableMemo( - "Send me 1 USDC to swap to 1 VIRTUAL", - MemoType.PAYABLE_REQUEST, - new FareAmount((job.requirement as Record)?.amount, config.baseFare), - job.providerAddress - ); + default: + console.warn("[handleTaskRequest] Unsupported job name", { jobId, jobName }); } - - console.error("Job name not supported", jobName); - return; }; -const handleTaskTransaction = async (job: AcpJob, memoToSign?: AcpMemo) => { - const jobName = job.name; - if (!jobName) { - console.error("job name not found", job); - return; - } - - if (jobName === JobName.OPEN_POSITION.toString()) { - const wallet = getClientWallet(job.clientAddress); - - const position = wallet.positions.find((p) => p.symbol === "USDC"); - - if (position) { - position.amount += 1; - } else { - wallet.positions.push({ - symbol: "USDC", - amount: 1, - }); - } +const handleTaskTransaction = async (job: AcpJob) => { + const { id: jobId, name: jobName } = job; + const wallet = getClientWallet(job.clientAddress); - await job.deliver({ - type: "message", - value: "Opened position with hash 0x1234567890", - }); + if (!jobName) { + console.error("[handleTaskTransaction] Missing job name", { jobId }); return; } - if (jobName === JobName.CLOSE_POSITION.toString()) { - const wallet = getClientWallet(job.clientAddress); - const position = wallet.positions.find((p) => p.symbol === "USDC"); - wallet.positions = wallet.positions.filter((p) => p.symbol !== "USDC"); - - const asset = wallet.assets.find( - (a) => a.fare.contractAddress === config.baseFare.contractAddress - ); - if (!asset) { - wallet.assets.push( - new FareAmount(position?.amount || 0, config.baseFare) + switch (jobName) { + case JobName.OPEN_POSITION: + adjustPosition( + wallet, + (job.requirement as V2DemoOpenPositionPayload)?.symbol, + Number((job.requirement as V2DemoOpenPositionPayload)?.amount) ); - } else { - asset.amount += BigInt(position?.amount || 0); - } - - await job.deliver({ - type: "message", - value: "Closed position with hash 0x1234567890", - }); - return; + console.log(wallet); + return job.deliver({ type: "message", value: "Opened position with hash 0x123..." }); + + case JobName.CLOSE_POSITION: + const closingAmount = closePosition(wallet, (job.requirement as V2DemoClosePositionPayload)?.symbol) || 0; + console.log(wallet); + await job.createRequirementPayableMemo( + `Close ${(job.requirement as V2DemoClosePositionPayload)?.symbol} position as per requested`, + MemoType.PAYABLE_TRANSFER_ESCROW, + new FareAmount( + closingAmount, + config.baseFare + ), + job.clientAddress, + ) + return job.deliver({ type: "message", value: "Closed position with hash 0x123..." }); + + case JobName.SWAP_TOKEN: + await job.createRequirementPayableMemo( + `Return swapped token ${(job.requirement as V2DemoSwapTokenPayload)?.toSymbol}`, + MemoType.PAYABLE_TRANSFER_ESCROW, + new FareAmount( + 1, + await Fare.fromContractAddress( // Constructing Fare for the token to swap to + (job.requirement as V2DemoSwapTokenPayload)?.toContractAddress, + baseSepoliaAcpConfig + ) + ), + job.clientAddress, + ) + return job.deliver({ type: "message", value: "Swapped token with hash 0x123..." }); + + default: + console.warn("[handleTaskTransaction] Unsupported job name", { jobId, jobName }); } +}; - if (jobName === JobName.SWAP_TOKEN.toString()) { - const wallet = getClientWallet(job.clientAddress); - const asset = wallet.assets.find( - (a) => a.fare.contractAddress === config.baseFare.contractAddress - ); - if (!asset) { - wallet.assets.push(new FareAmount(1, config.baseFare)); - } else { - asset.amount += BigInt(1); - } - - await job.deliver({ - type: "message", - value: "Swapped token with hash 0x1234567890", - }); - return; - } +function adjustPosition(wallet: IClientWallet, symbol: string, delta: number) { + const pos = wallet.positions.find((p) => p.symbol === symbol); + if (pos) pos.amount += delta; + else wallet.positions.push({ symbol, amount: delta }); +} - console.error("Job name not supported", jobName); - return; -}; +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({ diff --git a/examples/acp-base/polling-mode/buyer.ts b/examples/acp-base/polling-mode/buyer.ts index 572fa61..ecad902 100644 --- a/examples/acp-base/polling-mode/buyer.ts +++ b/examples/acp-base/polling-mode/buyer.ts @@ -81,7 +81,7 @@ async function buyer() { for (const memo of job.memos) { if (memo.nextPhase === AcpJobPhases.TRANSACTION) { console.log("Paying job", jobId); - await job.pay(job.price); + await job.payAndAcceptRequirement(); } } } else if (job.phase === AcpJobPhases.REQUEST) { diff --git a/examples/acp-base/self-evaluation-v2/README.md b/examples/acp-base/self-evaluation-v2/README.md index b575b02..74317e7 100644 --- a/examples/acp-base/self-evaluation-v2/README.md +++ b/examples/acp-base/self-evaluation-v2/README.md @@ -142,4 +142,3 @@ const acpClient = new AcpClient({ - 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 index fe5b7dd..8445bd5 100644 --- a/examples/acp-base/self-evaluation-v2/buyer.ts +++ b/examples/acp-base/self-evaluation-v2/buyer.ts @@ -15,23 +15,28 @@ import { } from "./env"; async function buyer() { - const config = baseSepoliaAcpConfig; - const acpClient = new AcpClient({ acpContractClient: await AcpContractClient.build( WHITELISTED_WALLET_PRIVATE_KEY, BUYER_ENTITY_ID, BUYER_AGENT_WALLET_ADDRESS, - config // v2 requires config parameter + baseSepoliaAcpConfig ), - onNewTask: async (job: AcpJob, memoToSign?: AcpMemo) => { // v2 has memoToSign parameter + onNewTask: async (job: AcpJob, memoToSign?: AcpMemo) => { if ( job.phase === AcpJobPhases.NEGOTIATION && - job.memos.find((m) => m.nextPhase === AcpJobPhases.TRANSACTION) + memoToSign?.nextPhase === AcpJobPhases.TRANSACTION ) { console.log("Paying job", job); - await job.pay(job.price); + 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); + 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`); } else if (job.phase === AcpJobPhases.REJECTED) { @@ -47,7 +52,7 @@ async function buyer() { // Browse available agents based on a keyword const relevantAgents = await acpClient.browseAgents( - "", // v2 example uses fund_provider + "", { sort_by: [AcpAgentSort.SUCCESSFUL_JOB_COUNT], top_k: 5, @@ -64,7 +69,7 @@ async function buyer() { const chosenJobOffering = chosenAgent.jobOfferings[0]; // v2 uses jobOfferings instead of offerings const jobId = await chosenJobOffering.initiateJob( - { "": "" }, // Use object to match schema requirement + { "": "" }, BUYER_AGENT_WALLET_ADDRESS, // evaluator address new Date(Date.now() + 1000 * 60 * 60 * 24) // expiredAt ); diff --git a/examples/acp-base/self-evaluation-v2/seller.ts b/examples/acp-base/self-evaluation-v2/seller.ts index 83bff1c..0358994 100644 --- a/examples/acp-base/self-evaluation-v2/seller.ts +++ b/examples/acp-base/self-evaluation-v2/seller.ts @@ -1,7 +1,7 @@ -import AcpClient, { - AcpContractClient, - AcpJobPhases, +import AcpClient, { + AcpContractClient, AcpJob, + AcpJobPhases, AcpMemo, baseSepoliaAcpConfig } from '../../../src'; @@ -12,27 +12,30 @@ import { } from "./env"; async function seller() { - const config = baseSepoliaAcpConfig; - - const acpClient = new AcpClient({ + new AcpClient({ acpContractClient: await AcpContractClient.build( WHITELISTED_WALLET_PRIVATE_KEY, SELLER_ENTITY_ID, SELLER_AGENT_WALLET_ADDRESS, - config // v2 requires config parameter + baseSepoliaAcpConfig ), - onNewTask: async (job: AcpJob, memoToSign?: AcpMemo) => { // v2 has memoToSign parameter + onNewTask: async (job: AcpJob, memoToSign?: AcpMemo) => { if ( job.phase === AcpJobPhases.REQUEST && - job.memos.find((m) => m.nextPhase === AcpJobPhases.NEGOTIATION) + memoToSign?.nextPhase === AcpJobPhases.NEGOTIATION ) { console.log("Responding to job", job); await job.respond(true); console.log(`Job ${job.id} responded`); } else if ( job.phase === AcpJobPhases.TRANSACTION && - job.memos.find((m) => m.nextPhase === AcpJobPhases.EVALUATION) + memoToSign?.nextPhase === AcpJobPhases.EVALUATION ) { + // // to cater cases where agent decide to reject job after payment has been made + // console.log("Rejecting job", job) + // await job.reject("Job requirement does not meet agent capability"); + // console.log(`Job ${job.id} rejected`); + console.log("Delivering job", job); await job.deliver( { From 04b419edcc579fb7e76fcc2bf72ddb836ea41502 Mon Sep 17 00:00:00 2001 From: Zuhwa Date: Thu, 2 Oct 2025 22:15:12 +0800 Subject: [PATCH 21/41] implement acp v2 contract --- src/acpAccount.ts | 21 + src/acpClient.ts | 278 ++++-- src/acpContractClient.ts | 392 --------- src/acpFare.ts | 2 +- src/acpJob.ts | 146 +++- src/acpJobOffering.ts | 53 +- src/acpMemo.ts | 17 +- src/{ => aibs}/acpAbi.ts | 0 src/aibs/acpAbiV2.ts | 840 +++++++++++++++++++ src/{ => aibs}/wethAbi.ts | 0 src/{ => configs}/acpConfigs.ts | 27 +- src/contractClients/acpContractClient.ts | 210 +++++ src/contractClients/acpContractClientV2.ts | 198 +++++ src/contractClients/baseAcpContractClient.ts | 277 ++++++ src/index.ts | 13 +- src/interfaces.ts | 17 +- 16 files changed, 1960 insertions(+), 531 deletions(-) create mode 100644 src/acpAccount.ts delete mode 100644 src/acpContractClient.ts rename src/{ => aibs}/acpAbi.ts (100%) create mode 100644 src/aibs/acpAbiV2.ts rename src/{ => aibs}/wethAbi.ts (100%) rename src/{ => configs}/acpConfigs.ts (56%) create mode 100644 src/contractClients/acpContractClient.ts create mode 100644 src/contractClients/acpContractClientV2.ts create mode 100644 src/contractClients/baseAcpContractClient.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 c2b4212..59c3d59 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, @@ -28,6 +29,8 @@ import { FareBigInt, wethFare, } from "./acpFare"; +import { AcpAccount } from "./acpAccount"; +import { baseAcpConfig, baseSepoliaAcpConfig } from "./configs/acpConfigs"; const { version } = require("../package.json"); @@ -56,35 +59,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"], }); @@ -110,7 +156,7 @@ class AcpClient { data.priceTokenAddress, data.memos.map((memo) => { return new AcpMemo( - this, + this.contractClientByAddress(data.contractAddress), memo.id, memo.memoType, memo.content, @@ -124,7 +170,8 @@ class AcpClient { ); }), data.phase, - data.context + data.context, + data.contractAddress ); this.onEvaluate(job); @@ -148,7 +195,7 @@ class AcpClient { data.priceTokenAddress, data.memos.map((memo) => { return new AcpMemo( - this, + this.contractClientByAddress(data.contractAddress), memo.id, memo.memoType, memo.content, @@ -162,7 +209,8 @@ class AcpClient { ); }), data.phase, - data.context + data.context, + data.contractAddress ); this.onNewTask( @@ -197,8 +245,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) { @@ -218,25 +266,51 @@ class AcpClient { data: AcpAgent[]; } = await response.json(); - return data.data.map((agent) => { - return { - id: agent.id, - name: agent.name, - description: agent.description, - jobOfferings: agent.jobs.map((jobs) => { - return new AcpJobOffering( - this, - agent.walletAddress, - jobs.name, - jobs.price, - jobs.requirement - ); - }), - 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( @@ -246,29 +320,42 @@ class AcpClient { 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, + 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 @@ -483,12 +570,12 @@ class AcpClient { async rejectJob(jobId: number, reason?: string) { return await this.acpContractClient.createMemo( - jobId, - `Job ${jobId} rejected. ${reason || ''}`, - MemoType.MESSAGE, - false, - AcpJobPhases.REJECTED - ) + jobId, + `Job ${jobId} rejected. ${reason || ""}`, + MemoType.MESSAGE, + false, + AcpJobPhases.REJECTED + ); } async deliverJob(jobId: number, deliverable: IDeliverable) { @@ -507,7 +594,7 @@ class AcpClient { try { const response = await fetch(url, { headers: { - "wallet-address": this.acpContractClient.walletAddress, + "wallet-address": this.walletAddress, }, }); @@ -528,7 +615,7 @@ class AcpClient { job.priceTokenAddress, job.memos.map((memo) => { return new AcpMemo( - this, + this.contractClientByAddress(job.contractAddress), memo.id, memo.memoType, memo.content, @@ -540,7 +627,8 @@ class AcpClient { ); }), job.phase, - job.context + job.context, + job.contractAddress ); }); } catch (error) { @@ -575,7 +663,7 @@ class AcpClient { job.priceTokenAddress, job.memos.map((memo) => { return new AcpMemo( - this, + this.contractClientByAddress(job.contractAddress), memo.id, memo.memoType, memo.content, @@ -587,7 +675,8 @@ class AcpClient { ); }), job.phase, - job.context + job.context, + job.contractAddress ); }); } catch (error) { @@ -601,7 +690,7 @@ class AcpClient { try { const response = await fetch(url, { headers: { - "wallet-address": this.acpContractClient.walletAddress, + "wallet-address": this.walletAddress, }, }); @@ -621,7 +710,7 @@ class AcpClient { job.priceTokenAddress, job.memos.map((memo) => { return new AcpMemo( - this, + this.contractClientByAddress(job.contractAddress), memo.id, memo.memoType, memo.content, @@ -633,7 +722,8 @@ class AcpClient { ); }), job.phase, - job.context + job.context, + job.contractAddress ); }); } catch (error) { @@ -672,7 +762,7 @@ class AcpClient { job.priceTokenAddress, job.memos.map((memo) => { return new AcpMemo( - this, + this.contractClientByAddress(job.contractAddress), memo.id, memo.memoType, memo.content, @@ -684,7 +774,8 @@ class AcpClient { ); }), job.phase, - job.context + job.context, + job.contractAddress ); } catch (error) { throw new AcpError("Failed to get job by id", error); @@ -697,7 +788,7 @@ class AcpClient { try { const response = await fetch(url, { headers: { - "wallet-address": this.acpContractClient.walletAddress, + "wallet-address": this.walletAddress, }, }); @@ -713,7 +804,7 @@ class AcpClient { } return new AcpMemo( - this, + this.contractClientByAddress(memo.contractAddress), memo.id, memo.memoType, memo.content, @@ -744,6 +835,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 0eacc9c..60e971b 100644 --- a/src/acpFare.ts +++ b/src/acpFare.ts @@ -7,7 +7,7 @@ import { parseUnits, } from "viem"; import AcpError from "./acpError"; -import { AcpContractConfig, baseAcpConfig } from "./acpConfigs"; +import { AcpContractConfig, baseAcpConfig } from "./configs/acpConfigs"; class Fare { constructor(public contractAddress: Address, public decimals: number) {} diff --git a/src/acpJob.ts b/src/acpJob.ts index 0e10bc2..08c836a 100644 --- a/src/acpJob.ts +++ b/src/acpJob.ts @@ -1,6 +1,10 @@ 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, @@ -19,8 +23,6 @@ import { Fare, FareAmount, FareAmountBase } from "./acpFare"; import AcpError from "./acpError"; class AcpJob { - private baseFare: Fare; - public name: string | undefined; public requirement: Record | string | undefined; @@ -34,10 +36,9 @@ 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; - const content = this.memos.find( (m) => m.nextPhase === AcpJobPhases.NEGOTIATION )?.content; @@ -67,6 +68,18 @@ class AcpJob { } } + public get acpContractClient() { + return this.acpClient.contractClientByAddress(this.contractAddress); + } + + public get config() { + return this.acpContractClient.config; + } + + public get baseFare() { + return this.acpContractClient.config.baseFare; + } + public get deliverable() { return this.memos.find((m) => m.nextPhase === AcpJobPhases.COMPLETED) ?.content; @@ -83,14 +96,21 @@ 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 createRequirementMemo(content: string) { - return await this.acpClient.createMemo( + return await this.acpContractClient.createMemo( this.id, content, + MemoType.MESSAGE, + false, AcpJobPhases.TRANSACTION ); } @@ -102,14 +122,26 @@ class AcpJob { recipient: Address, expiredAt: Date = new Date(Date.now() + 1000 * 60 * 5) // 5 minutes ) { - return await this.acpClient.createPayableMemo( + 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.amount, recipient, + feeAmount.amount, + FeeType.NO_FEE, AcpJobPhases.TRANSACTION, type, - expiredAt + expiredAt, + amount.fare.contractAddress ); } @@ -119,7 +151,7 @@ class AcpJob { ); if (!memo) { - throw new Error("No transaction memo found"); + throw new AcpError("No transaction memo found"); } const baseFareAmount = new FareAmount(this.price, this.baseFare); @@ -127,7 +159,7 @@ class AcpJob { ? await FareAmountBase.fromContractAddress( memo.payableDetails.amount, memo.payableDetails.token, - this.acpClient.acpContractClient.config + this.config ) : new FareAmount(0, this.baseFare); @@ -137,7 +169,7 @@ class AcpJob { ? baseFareAmount.add(transferAmount) : baseFareAmount; - await this.acpClient.acpContractClient.approveAllowance( + await this.acpContractClient.approveAllowance( totalAmount.amount, this.baseFare.contractAddress ); @@ -146,7 +178,7 @@ class AcpJob { baseFareAmount.fare.contractAddress !== transferAmount.fare.contractAddress ) { - await this.acpClient.acpContractClient.approveAllowance( + await this.acpContractClient.approveAllowance( transferAmount.amount, transferAmount.fare.contractAddress ); @@ -154,53 +186,53 @@ class AcpJob { await memo.sign(true, reason); - return await this.acpClient.createMemo( + return await this.acpContractClient.createMemo( this.id, `Payment made. ${reason ?? ""}`.trim(), + MemoType.MESSAGE, + true, AcpJobPhases.EVALUATION ); } - async pay(amount: number, reason?: string) { - const memo = this.memos.find( - (m) => m.nextPhase === AcpJobPhases.TRANSACTION - ); - - if (!memo) { - throw new AcpError("No transaction memo found"); + async respond(accept: boolean, reason?: string) { + if (accept) { + return await this.accept(reason); } - return await this.acpClient.payJob( - this.id, - this.baseFare.formatAmount(amount), - memo.id, - reason - ); + return await this.reject(reason); } - async respond( - accept: boolean, - payload?: GenericPayload, - reason?: string - ) { + 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) { - return await this.acpClient.rejectJob( - this.id, - `Job ${this.id} rejected. ${reason || ''}`, - ) + 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, + `Job ${this.id} rejected. ${reason || ""}` + ); } async deliver(deliverable: IDeliverable) { @@ -208,7 +240,13 @@ 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 evaluate(accept: boolean, reason?: string) { @@ -216,9 +254,25 @@ 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); + } + + // to be deprecated + async pay(amount: number, 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(amount), + memo.id, reason ); } diff --git a/src/acpJobOffering.ts b/src/acpJobOffering.ts index f5df1f7..d7b2d7f 100644 --- a/src/acpJobOffering.ts +++ b/src/acpJobOffering.ts @@ -3,12 +3,18 @@ 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, @@ -47,16 +53,47 @@ class AcpJobOffering { }; } - return await this.acpClient.initiateJob( + const fareAmount = new FareAmount( + this.price, + this.acpContractClient.config.baseFare + ); + + 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, + evaluatorAddress || this.acpContractClient.walletAddress, + fareAmount.amount, + fareAmount.fare.contractAddress, + expiredAt + ); + + await this.acpContractClient.createMemo( + jobId, + JSON.stringify(serviceRequirement), + MemoType.MESSAGE, + true, + AcpJobPhases.NEGOTIATION ); + + return jobId; } } diff --git a/src/acpMemo.ts b/src/acpMemo.ts index 8087a4a..e5b1696 100644 --- a/src/acpMemo.ts +++ b/src/acpMemo.ts @@ -1,6 +1,7 @@ -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 +14,7 @@ class AcpMemo { structuredContent: GenericPayload | undefined; constructor( - private acpClient: AcpClient, + private contractClient: BaseAcpContractClient, public id: number, public type: MemoType, public content: string, @@ -41,7 +42,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, @@ -51,11 +52,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/acpAbi.ts b/src/aibs/acpAbi.ts similarity index 100% rename from src/acpAbi.ts rename to src/aibs/acpAbi.ts diff --git a/src/aibs/acpAbiV2.ts b/src/aibs/acpAbiV2.ts new file mode 100644 index 0000000..d6a5f57 --- /dev/null +++ b/src/aibs/acpAbiV2.ts @@ -0,0 +1,840 @@ +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: "uint8", 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: "uint8", 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: [ + { internalType: "uint256", name: "accountId", type: "uint256" }, + { internalType: "uint256", name: "jobId", type: "uint256" }, + ], + name: "markJobCompleted", + outputs: [], + stateMutability: "nonpayable", + 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: "uint256", name: "jobId", type: "uint256" }, + { + internalType: "enum ACPTypes.JobPhase", + name: "newPhase", + type: "uint8", + }, + ], + name: "updateJobPhase", + 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/wethAbi.ts b/src/aibs/wethAbi.ts similarity index 100% rename from src/wethAbi.ts rename to src/aibs/wethAbi.ts diff --git a/src/acpConfigs.ts b/src/configs/acpConfigs.ts similarity index 56% rename from src/acpConfigs.ts rename to src/configs/acpConfigs.ts index 7fb7843..a8a197c 100644 --- a/src/acpConfigs.ts +++ b/src/configs/acpConfigs.ts @@ -1,6 +1,8 @@ import { Address } from "@aa-sdk/core"; import { baseSepolia, base } from "@account-kit/infra"; -import { Fare } from "./acpFare"; +import { Fare } from "../acpFare"; +import { ACP_ABI } from "../../dist"; +import ACP_V2_ABI from "../aibs/acpAbiV2"; class AcpContractConfig { constructor( @@ -9,6 +11,7 @@ class AcpContractConfig { public baseFare: Fare, public alchemyRpcUrl: string, public acpUrl: string, + public abi: typeof ACP_ABI | typeof ACP_V2_ABI, public rpcEndpoint?: string ) {} } @@ -18,7 +21,17 @@ const baseSepoliaAcpConfig = new AcpContractConfig( "0x8Db6B1c839Fc8f6bd35777E194677B67b4D51928", new Fare("0x036CbD53842c5426634e7929541eC2318f3dCF7e", 6), "https://alchemy-proxy.virtuals.io/api/proxy/rpc", - "https://acpx.virtuals.gg" + "https://acpx.virtuals.gg", + ACP_ABI +); + +const baseSepoliaAcpConfigV2 = new AcpContractConfig( + baseSepolia, + "0xd56F89058F88A97a997cf029793F02f84860c5a1", + new Fare("0x036CbD53842c5426634e7929541eC2318f3dCF7e", 6), + "https://alchemy-proxy.virtuals.io/api/proxy/rpc", + "https://acpx.virtuals.gg", + ACP_V2_ABI ); const baseAcpConfig = new AcpContractConfig( @@ -26,7 +39,13 @@ const baseAcpConfig = new AcpContractConfig( "0x6a1FE26D54ab0d3E1e3168f2e0c0cDa5cC0A0A4A", new Fare("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", 6), "https://alchemy-proxy-prod.virtuals.io/api/proxy/rpc", - "https://acpx.virtuals.io" + "https://acpx.virtuals.io", + ACP_ABI ); -export { AcpContractConfig, baseSepoliaAcpConfig, baseAcpConfig }; +export { + AcpContractConfig, + baseSepoliaAcpConfigV2, + baseSepoliaAcpConfig, + baseAcpConfig, +}; diff --git a/src/contractClients/acpContractClient.ts b/src/contractClients/acpContractClient.ts new file mode 100644 index 0000000..f31fc56 --- /dev/null +++ b/src/contractClients/acpContractClient.ts @@ -0,0 +1,210 @@ +import { Address, LocalAccountSigner, SmartAccountSigner } from "@aa-sdk/core"; +import { alchemy } from "@account-kit/infra"; +import { + ModularAccountV2Client, + createModularAccountV2Client, +} from "@account-kit/smart-contracts"; +import { encodeFunctionData, fromHex } from "viem"; +import { AcpContractConfig, baseAcpConfig } from "../configs/acpConfigs"; +import AcpError from "../acpError"; +import BaseAcpContractClient 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) { + const result = await this.sessionKeyClient.getUserOperationReceipt( + hash, + "pending" + ); + + 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, + 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); + + await this.setBudgetWithPaymentToken( + jobId, + budgetBaseUnit, + paymentTokenAddress + ); + + return { txHash: hash, jobId: jobId }; + } catch (error) { + throw new AcpError("Failed to create job", error); + } + } + + async createJobWithAccount( + accountId: number, + 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..ad76b21 --- /dev/null +++ b/src/contractClients/acpContractClientV2.ts @@ -0,0 +1,198 @@ +import { Address, LocalAccountSigner, SmartAccountSigner } from "@aa-sdk/core"; +import { alchemy } from "@account-kit/infra"; +import { + ModularAccountV2Client, + createModularAccountV2Client, +} from "@account-kit/smart-contracts"; +import { createPublicClient, fromHex, http } from "viem"; +import { AcpContractConfig, baseAcpConfig } from "../configs/acpConfigs"; +import AcpError from "../acpError"; +import BaseAcpContractClient from "./baseAcpContractClient"; + +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 = baseAcpConfig + ) { + super(agentWalletAddress, config); + } + + static async build( + walletPrivateKey: Address, + sessionEntityKeyId: number, + agentWalletAddress: Address, + config: AcpContractConfig = baseAcpConfig + ) { + 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) { + 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.jobManagerAddress.toLowerCase() + ) as any; + + if (!contractLogs) { + throw new AcpError("Failed to get contract logs"); + } + + return fromHex(contractLogs.topics[1], "number"); + } +} + +export default AcpContractClientV2; diff --git a/src/contractClients/baseAcpContractClient.ts b/src/contractClients/baseAcpContractClient.ts new file mode 100644 index 0000000..3d9b252 --- /dev/null +++ b/src/contractClients/baseAcpContractClient.ts @@ -0,0 +1,277 @@ +import { + Address, + Chain, + encodeFunctionData, + erc20Abi, + zeroAddress, +} from "viem"; +import { AcpContractConfig, baseAcpConfig } from "../configs/acpConfigs"; +import ACP_V2_ABI from "../aibs/acpAbiV2"; +import ACP_ABI from "../aibs/acpAbi"; +import AcpError from "../acpError"; +import WETH_ABI from "../aibs/wethAbi"; +import { wethFare } from "../acpFare"; + +export enum MemoType { + MESSAGE, + CONTEXT_URL, + IMAGE_URL, + VOICE_URL, + OBJECT_URL, + TXHASH, + PAYABLE_REQUEST, + PAYABLE_TRANSFER, + PAYABLE_TRANSFER_ESCROW, + 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; + + constructor( + public agentWalletAddress: Address, + public config: AcpContractConfig = baseAcpConfig + ) { + this.chain = config.chain; + this.abi = config.abi; + this.contractAddress = config.contractAddress; + } + + abstract handleOperation( + data: `0x${string}`, + contractAddress: Address, + value?: bigint + ): Promise
; + + abstract getJobId(hash: Address): Promise; + + get walletAddress() { + return this.agentWalletAddress; + } + + async createJobWithAccount( + accountId: number, + 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 === this.agentWalletAddress + ? zeroAddress + : evaluatorAddress, + budgetBaseUnit, + paymentTokenAddress, + expiredAt, + ], + }); + + const hash = await this.handleOperation(data, this.contractAddress); + + const jobId = await this.getJobId(hash); + + return { txHash: hash, jobId: jobId }; + } catch (error) { + throw new AcpError("Failed to create job with account", error); + } + } + + async createJob( + providerAddress: string, + evaluatorAddress: string, + 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 === this.agentWalletAddress + ? zeroAddress + : evaluatorAddress, + Math.floor(expireAt.getTime() / 1000), + paymentTokenAddress, + budgetBaseUnit, + metadata, + ], + }); + + const hash = await this.handleOperation(data, this.contractAddress); + + 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.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, + expiredAt: Date, + token: Address = this.config.baseFare.contractAddress + ) { + 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 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); + + console.log("Signed memo", memoId, isApproved, reason, hash); + + 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..54d7141 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,10 @@ -import ACP_ABI from "./acpAbi"; +import ACP_ABI from "./aibs/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 { @@ -22,15 +26,18 @@ import { AcpContractConfig, baseAcpConfig, baseSepoliaAcpConfig, -} from "./acpConfigs"; +} 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, diff --git a/src/interfaces.ts b/src/interfaces.ts index 8e6b782..af6a2c0 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"; @@ -32,6 +35,7 @@ export interface IAcpMemoData { signedReason?: string; expiry?: string; payableDetails?: PayableDetails; + contractAddress?: Address; } export interface IAcpMemo { data: IAcpMemoData; @@ -71,6 +75,7 @@ export interface IAcpJob { memos: IAcpMemoData[]; context: Record; createdAt: string; + contractAddress: Address; memoToSign?: number; }; error?: Error; @@ -89,7 +94,7 @@ export interface IAcpJobResponse { } export interface IAcpClientOptions { - acpContractClient: AcpContractClient; + acpContractClient: AcpContractClient | AcpContractClient[]; onNewTask?: (job: AcpJob, memoToSign?: acpMemo) => void; onEvaluate?: (job: AcpJob) => void; customRpcUrl?: string; @@ -130,6 +135,14 @@ export type AcpAgent = { minsFromLastOnline: number; isOnline: boolean; }; + contractAddress: Address; +}; + +export type IAcpAccount = { + id: number; + clientAddress: Address; + providerAddress: Address; + metadata: Record; }; export enum PayloadType { From 2e27eee68706229943ae48558a95b752e08fb139 Mon Sep 17 00:00:00 2001 From: Zuhwa Date: Thu, 2 Oct 2025 22:19:58 +0800 Subject: [PATCH 22/41] resolve conflict --- src/acpClient.ts | 5 +++-- src/acpMemo.ts | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/acpClient.ts b/src/acpClient.ts index 9c412d3..4827542 100644 --- a/src/acpClient.ts +++ b/src/acpClient.ts @@ -667,7 +667,7 @@ class AcpClient { job.priceTokenAddress, job.memos.map((memo) => { return new AcpMemo( - this, + this.contractClientByAddress(job.contractAddress), memo.id, memo.memoType, memo.content, @@ -682,7 +682,8 @@ class AcpClient { ); }), job.phase, - job.context + job.context, + job.contractAddress ); }); } catch (error) { diff --git a/src/acpMemo.ts b/src/acpMemo.ts index 516e6e2..3d78cd7 100644 --- a/src/acpMemo.ts +++ b/src/acpMemo.ts @@ -1,3 +1,4 @@ +import { Address } from "viem"; import BaseAcpContractClient, { AcpJobPhases, MemoType, From ce9e64f566eb6b4f286a823975f6ee0de1ddeeb7 Mon Sep 17 00:00:00 2001 From: Zuhwa Date: Thu, 2 Oct 2025 22:22:04 +0800 Subject: [PATCH 23/41] removed unused console log --- src/contractClients/baseAcpContractClient.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/contractClients/baseAcpContractClient.ts b/src/contractClients/baseAcpContractClient.ts index 3d9b252..3fb6d2c 100644 --- a/src/contractClients/baseAcpContractClient.ts +++ b/src/contractClients/baseAcpContractClient.ts @@ -216,8 +216,6 @@ abstract class BaseAcpContractClient { const hash = await this.handleOperation(data, this.contractAddress); - console.log("Signed memo", memoId, isApproved, reason, hash); - return hash; } catch (error) { throw new AcpError("Failed to sign memo", error); From e4df7ce9654104cdf1d8f311170b363577ade99d Mon Sep 17 00:00:00 2001 From: Zuhwa Date: Fri, 3 Oct 2025 10:41:15 +0800 Subject: [PATCH 24/41] notification memo --- src/acpJob.ts | 12 +++++- src/acpJobOffering.ts | 4 +- src/contractClients/baseAcpContractClient.ts | 40 +++++++++----------- 3 files changed, 30 insertions(+), 26 deletions(-) diff --git a/src/acpJob.ts b/src/acpJob.ts index 08c836a..1d964d5 100644 --- a/src/acpJob.ts +++ b/src/acpJob.ts @@ -259,7 +259,6 @@ class AcpJob { await memo.sign(accept, reason); } - // to be deprecated async pay(amount: number, reason?: string) { const memo = this.memos.find( (m) => m.nextPhase === AcpJobPhases.TRANSACTION @@ -277,6 +276,17 @@ class AcpJob { ); } + async createNotification(content: string) { + return await this.acpContractClient.createMemo( + this.id, + content, + MemoType.FEEDBACK, + true, + AcpJobPhases.COMPLETED + ); + } + + // to be deprecated async openPosition( payload: OpenPositionPayload[], feeAmount: number, diff --git a/src/acpJobOffering.ts b/src/acpJobOffering.ts index d7b2d7f..6d2eae8 100644 --- a/src/acpJobOffering.ts +++ b/src/acpJobOffering.ts @@ -1,4 +1,4 @@ -import { Address } from "viem"; +import { Address, zeroAddress } from "viem"; import AcpClient from "./acpClient"; import Ajv from "ajv"; import { FareAmount } from "./acpFare"; @@ -79,7 +79,7 @@ class AcpJobOffering { ) : await this.acpContractClient.createJobWithAccount( account.id, - evaluatorAddress || this.acpContractClient.walletAddress, + evaluatorAddress || zeroAddress, fareAmount.amount, fareAmount.fare.contractAddress, expiredAt diff --git a/src/contractClients/baseAcpContractClient.ts b/src/contractClients/baseAcpContractClient.ts index 3fb6d2c..6fcc0fb 100644 --- a/src/contractClients/baseAcpContractClient.ts +++ b/src/contractClients/baseAcpContractClient.ts @@ -1,10 +1,4 @@ -import { - Address, - Chain, - encodeFunctionData, - erc20Abi, - zeroAddress, -} from "viem"; +import { Address, Chain, encodeFunctionData, erc20Abi } from "viem"; import { AcpContractConfig, baseAcpConfig } from "../configs/acpConfigs"; import ACP_V2_ABI from "../aibs/acpAbiV2"; import ACP_ABI from "../aibs/acpAbi"; @@ -13,16 +7,20 @@ import WETH_ABI from "../aibs/wethAbi"; import { wethFare } from "../acpFare"; export enum MemoType { - MESSAGE, - CONTEXT_URL, - IMAGE_URL, - VOICE_URL, - OBJECT_URL, - TXHASH, - PAYABLE_REQUEST, - PAYABLE_TRANSFER, - PAYABLE_TRANSFER_ESCROW, - NOTIFICATION, + 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 + MILESTONE_PROPOSAL, // 9 - Milestone proposal + MILESTONE_COMPLETION, // 10 - Milestone completion claim + DELIVERABLE_SUBMISSION, // 11 - Deliverable submission + FEEDBACK, // 12 - temp for notification + REVISION_REQUEST, // 13 - Request for revisions } export enum AcpJobPhases { @@ -80,9 +78,7 @@ abstract class BaseAcpContractClient { functionName: "createJobWithAccount", args: [ accountId, - evaluatorAddress === this.agentWalletAddress - ? zeroAddress - : evaluatorAddress, + evaluatorAddress, budgetBaseUnit, paymentTokenAddress, expiredAt, @@ -113,9 +109,7 @@ abstract class BaseAcpContractClient { functionName: "createJob", args: [ providerAddress, - evaluatorAddress === this.agentWalletAddress - ? zeroAddress - : evaluatorAddress, + evaluatorAddress, Math.floor(expireAt.getTime() / 1000), paymentTokenAddress, budgetBaseUnit, From 6279611f5ddfb8b98011261dd60d961b7648dbd2 Mon Sep 17 00:00:00 2001 From: Zuhwa Date: Fri, 3 Oct 2025 15:13:46 +0800 Subject: [PATCH 25/41] deprcated annotation --- src/acpJob.ts | 52 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/src/acpJob.ts b/src/acpJob.ts index 1d964d5..8e18080 100644 --- a/src/acpJob.ts +++ b/src/acpJob.ts @@ -287,6 +287,10 @@ class AcpJob { } // to be deprecated + + /** + * @deprecated The method should not be used + */ async openPosition( payload: OpenPositionPayload[], feeAmount: number, @@ -314,6 +318,9 @@ class AcpJob { ); } + /** + * @deprecated The method should not be used + */ async swapToken( payload: SwapTokenPayload, decimals: number, @@ -338,6 +345,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); @@ -359,6 +369,9 @@ class AcpJob { return await memo.sign(accept, reason); } + /** + * @deprecated The method should not be used + */ async transferFunds( payload: GenericPayload, fareAmount: FareAmountBase, @@ -377,6 +390,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); @@ -398,6 +414,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 @@ -417,6 +436,9 @@ class AcpJob { ); } + /** + * @deprecated The method should not be used + */ async responseClosePartialPosition( memoId: number, accept: boolean, @@ -447,6 +469,9 @@ class AcpJob { ); } + /** + * @deprecated The method should not be used + */ async requestClosePosition(payload: RequestClosePositionPayload) { return await this.acpClient.sendMessage( this.id, @@ -458,6 +483,9 @@ class AcpJob { ); } + /** + * @deprecated The method should not be used + */ async responseRequestClosePosition( memoId: number, accept: boolean, @@ -501,6 +529,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); @@ -522,6 +553,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 @@ -541,6 +575,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 @@ -560,6 +597,9 @@ class AcpJob { ); } + /** + * @deprecated The method should not be used + */ async responseUnfulfilledPosition( memoId: number, accept: boolean, @@ -585,6 +625,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, @@ -610,6 +653,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, @@ -623,6 +669,9 @@ class AcpJob { ); } + /** + * @deprecated The method should not be used + */ async responseCloseJob( memoId: number, accept: boolean, @@ -684,6 +733,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); From f523d489213b5c828768fef8b74cb5f240a6e1ed Mon Sep 17 00:00:00 2001 From: Ang Weoy Yang Date: Fri, 3 Oct 2025 15:26:36 +0800 Subject: [PATCH 26/41] chore: move examples to trading sub-directory --- examples/acp-base/funds-v2/{ => trading}/.env.example | 0 examples/acp-base/funds-v2/{ => trading}/buyer.ts | 2 +- examples/acp-base/funds-v2/{ => trading}/env.ts | 0 examples/acp-base/funds-v2/{ => trading}/jobTypes.ts | 0 examples/acp-base/funds-v2/{ => trading}/seller.ts | 4 +--- 5 files changed, 2 insertions(+), 4 deletions(-) rename examples/acp-base/funds-v2/{ => trading}/.env.example (100%) rename examples/acp-base/funds-v2/{ => trading}/buyer.ts (99%) rename examples/acp-base/funds-v2/{ => trading}/env.ts (100%) rename examples/acp-base/funds-v2/{ => trading}/jobTypes.ts (100%) rename examples/acp-base/funds-v2/{ => trading}/seller.ts (99%) diff --git a/examples/acp-base/funds-v2/.env.example b/examples/acp-base/funds-v2/trading/.env.example similarity index 100% rename from examples/acp-base/funds-v2/.env.example rename to examples/acp-base/funds-v2/trading/.env.example diff --git a/examples/acp-base/funds-v2/buyer.ts b/examples/acp-base/funds-v2/trading/buyer.ts similarity index 99% rename from examples/acp-base/funds-v2/buyer.ts rename to examples/acp-base/funds-v2/trading/buyer.ts index dfe3da2..b990a94 100644 --- a/examples/acp-base/funds-v2/buyer.ts +++ b/examples/acp-base/funds-v2/trading/buyer.ts @@ -9,7 +9,7 @@ import AcpClient, { AcpOnlineStatus, baseSepoliaAcpConfig, MemoType, -} from "../../../src"; +} from "../../../../src"; import { BUYER_AGENT_WALLET_ADDRESS, BUYER_ENTITY_ID, diff --git a/examples/acp-base/funds-v2/env.ts b/examples/acp-base/funds-v2/trading/env.ts similarity index 100% rename from examples/acp-base/funds-v2/env.ts rename to examples/acp-base/funds-v2/trading/env.ts diff --git a/examples/acp-base/funds-v2/jobTypes.ts b/examples/acp-base/funds-v2/trading/jobTypes.ts similarity index 100% rename from examples/acp-base/funds-v2/jobTypes.ts rename to examples/acp-base/funds-v2/trading/jobTypes.ts diff --git a/examples/acp-base/funds-v2/seller.ts b/examples/acp-base/funds-v2/trading/seller.ts similarity index 99% rename from examples/acp-base/funds-v2/seller.ts rename to examples/acp-base/funds-v2/trading/seller.ts index 4abdbb0..144f049 100644 --- a/examples/acp-base/funds-v2/seller.ts +++ b/examples/acp-base/funds-v2/trading/seller.ts @@ -8,7 +8,7 @@ import AcpClient, { Fare, FareAmount, MemoType, -} from "../../../src"; +} from "../../../../src"; import { Address } from "viem"; import { createHash } from "crypto"; import { @@ -39,7 +39,6 @@ interface IPosition { interface IClientWallet { clientAddress: Address; - assets: FareAmount[]; positions: IPosition[]; } @@ -52,7 +51,6 @@ const getClientWallet = (address: Address): IClientWallet => { if (!client[walletAddress]) { client[walletAddress] = { clientAddress: walletAddress, - assets: [], positions: [], }; } From d4feaafc9845729182f95ff90813e17070c48a10 Mon Sep 17 00:00:00 2001 From: Ang Weoy Yang Date: Fri, 3 Oct 2025 15:27:35 +0800 Subject: [PATCH 27/41] fix: import and function update --- src/configs/acpConfigs.ts | 2 +- src/contractClients/acpContractClient.ts | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/configs/acpConfigs.ts b/src/configs/acpConfigs.ts index a8a197c..8114413 100644 --- a/src/configs/acpConfigs.ts +++ b/src/configs/acpConfigs.ts @@ -1,7 +1,7 @@ import { Address } from "@aa-sdk/core"; import { baseSepolia, base } from "@account-kit/infra"; import { Fare } from "../acpFare"; -import { ACP_ABI } from "../../dist"; +import ACP_ABI from "../aibs/acpAbi"; import ACP_V2_ABI from "../aibs/acpAbiV2"; class AcpContractConfig { diff --git a/src/contractClients/acpContractClient.ts b/src/contractClients/acpContractClient.ts index f31fc56..77971d1 100644 --- a/src/contractClients/acpContractClient.ts +++ b/src/contractClients/acpContractClient.ts @@ -133,10 +133,7 @@ class AcpContractClient extends BaseAcpContractClient { } async getJobId(hash: Address) { - const result = await this.sessionKeyClient.getUserOperationReceipt( - hash, - "pending" - ); + const result = await this.sessionKeyClient.getUserOperationReceipt(hash); if (!result) { throw new AcpError("Failed to get user operation receipt"); From 3bbfd3258f0efe9f56c140ed140734b65c2985cc Mon Sep 17 00:00:00 2001 From: Ang Weoy Yang Date: Fri, 3 Oct 2025 17:06:45 +0800 Subject: [PATCH 28/41] fix: add back service name to job --- src/acpJobOffering.ts | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/src/acpJobOffering.ts b/src/acpJobOffering.ts index 6d2eae8..38c635d 100644 --- a/src/acpJobOffering.ts +++ b/src/acpJobOffering.ts @@ -37,20 +37,9 @@ class AcpJobOffering { } } - let finalServiceRequirement: Record = { + const finalServiceRequirement: Record = { name: this.name, - }; - - if (typeof serviceRequirement === "string") { - finalServiceRequirement = { - ...finalServiceRequirement, - requirement: serviceRequirement, - }; - } else { - finalServiceRequirement = { - ...finalServiceRequirement, - requirement: serviceRequirement, - }; + requirement: serviceRequirement, } const fareAmount = new FareAmount( @@ -87,7 +76,7 @@ class AcpJobOffering { await this.acpContractClient.createMemo( jobId, - JSON.stringify(serviceRequirement), + JSON.stringify(finalServiceRequirement), MemoType.MESSAGE, true, AcpJobPhases.NEGOTIATION From 6e3ddd29d2e65208ece6eec2fc7eeafeb0aebddc Mon Sep 17 00:00:00 2001 From: Ang Weoy Yang Date: Fri, 3 Oct 2025 17:28:40 +0800 Subject: [PATCH 29/41] chore: upgrade to V2 --- .../acp-base/external-evaluation-v2/buyer.ts | 8 ++-- .../external-evaluation-v2/evaluator.ts | 8 ++-- .../acp-base/external-evaluation-v2/seller.ts | 10 ++--- examples/acp-base/funds-v2/trading/buyer.ts | 8 ++-- examples/acp-base/funds-v2/trading/seller.ts | 44 +++++++++++-------- examples/acp-base/polling-mode/buyer.ts | 4 +- examples/acp-base/polling-mode/seller.ts | 4 +- examples/acp-base/self-evaluation-v2/buyer.ts | 4 +- .../acp-base/self-evaluation-v2/seller.ts | 4 +- src/index.ts | 2 + 10 files changed, 53 insertions(+), 43 deletions(-) diff --git a/examples/acp-base/external-evaluation-v2/buyer.ts b/examples/acp-base/external-evaluation-v2/buyer.ts index 01d17f9..f147b0d 100644 --- a/examples/acp-base/external-evaluation-v2/buyer.ts +++ b/examples/acp-base/external-evaluation-v2/buyer.ts @@ -1,12 +1,12 @@ import AcpClient, { - AcpContractClient, + AcpContractClientV2, AcpJobPhases, AcpJob, AcpMemo, AcpAgentSort, AcpGraduationStatus, AcpOnlineStatus, - baseSepoliaAcpConfig + baseSepoliaAcpConfigV2 } from "../../../src"; import { BUYER_AGENT_WALLET_ADDRESS, @@ -17,11 +17,11 @@ import { 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, - baseSepoliaAcpConfig + baseSepoliaAcpConfigV2 ), onNewTask: async (job: AcpJob, memoToSign?: AcpMemo) => { if ( diff --git a/examples/acp-base/external-evaluation-v2/evaluator.ts b/examples/acp-base/external-evaluation-v2/evaluator.ts index 3a655b8..78f9047 100644 --- a/examples/acp-base/external-evaluation-v2/evaluator.ts +++ b/examples/acp-base/external-evaluation-v2/evaluator.ts @@ -1,7 +1,7 @@ import AcpClient, { - AcpContractClient, + AcpContractClientV2, AcpJob, - baseSepoliaAcpConfig + baseSepoliaAcpConfigV2 } from '../../../src'; import { EVALUATOR_AGENT_WALLET_ADDRESS, @@ -11,11 +11,11 @@ import { async function evaluator() { new AcpClient({ - acpContractClient: await AcpContractClient.build( + acpContractClient: await AcpContractClientV2.build( WHITELISTED_WALLET_PRIVATE_KEY, EVALUATOR_ENTITY_ID, EVALUATOR_AGENT_WALLET_ADDRESS, - baseSepoliaAcpConfig + baseSepoliaAcpConfigV2 ), onEvaluate: async (job: AcpJob) => { console.log("[onEvaluate] Evaluation function called", job.memos); diff --git a/examples/acp-base/external-evaluation-v2/seller.ts b/examples/acp-base/external-evaluation-v2/seller.ts index 7e0b7e7..0d025ed 100644 --- a/examples/acp-base/external-evaluation-v2/seller.ts +++ b/examples/acp-base/external-evaluation-v2/seller.ts @@ -1,9 +1,9 @@ -import AcpClient, { - AcpContractClient, +import AcpClient, { + AcpContractClientV2, AcpJobPhases, AcpJob, AcpMemo, - baseSepoliaAcpConfig + baseSepoliaAcpConfigV2 } from '../../../src'; import { SELLER_AGENT_WALLET_ADDRESS, @@ -13,11 +13,11 @@ import { async function seller() { new AcpClient({ - acpContractClient: await AcpContractClient.build( + acpContractClient: await AcpContractClientV2.build( WHITELISTED_WALLET_PRIVATE_KEY, SELLER_ENTITY_ID, SELLER_AGENT_WALLET_ADDRESS, - baseSepoliaAcpConfig + baseSepoliaAcpConfigV2 ), onNewTask: async (job: AcpJob, memoToSign?: AcpMemo) => { if ( diff --git a/examples/acp-base/funds-v2/trading/buyer.ts b/examples/acp-base/funds-v2/trading/buyer.ts index b990a94..53e77a4 100644 --- a/examples/acp-base/funds-v2/trading/buyer.ts +++ b/examples/acp-base/funds-v2/trading/buyer.ts @@ -1,13 +1,13 @@ import * as readline from "readline"; import AcpClient, { AcpAgentSort, - AcpContractClient, + AcpContractClientV2, AcpGraduationStatus, AcpJob, AcpJobPhases, AcpMemo, AcpOnlineStatus, - baseSepoliaAcpConfig, + baseSepoliaAcpConfigV2, MemoType, } from "../../../../src"; import { @@ -43,11 +43,11 @@ async function main() { let currentJobId: number | null = null; const acpClient = new AcpClient({ - acpContractClient: await AcpContractClient.build( + acpContractClient: await AcpContractClientV2.build( WHITELISTED_WALLET_PRIVATE_KEY, BUYER_ENTITY_ID, BUYER_AGENT_WALLET_ADDRESS, - baseSepoliaAcpConfig + baseSepoliaAcpConfigV2 ), onNewTask: async (job: AcpJob, memoToSign?: AcpMemo) => { const { id: jobId, phase: jobPhase } = job; diff --git a/examples/acp-base/funds-v2/trading/seller.ts b/examples/acp-base/funds-v2/trading/seller.ts index 144f049..0fecd4e 100644 --- a/examples/acp-base/funds-v2/trading/seller.ts +++ b/examples/acp-base/funds-v2/trading/seller.ts @@ -1,10 +1,10 @@ import dotenv from "dotenv"; import AcpClient, { - AcpContractClient, + AcpContractClientV2, AcpJob, AcpJobPhases, AcpMemo, - baseSepoliaAcpConfig, + baseSepoliaAcpConfigV2, Fare, FareAmount, MemoType, @@ -24,7 +24,7 @@ import { dotenv.config(); -const config = baseSepoliaAcpConfig; +const config = baseSepoliaAcpConfigV2; enum JobName { OPEN_POSITION = "open_position", @@ -88,11 +88,12 @@ const handleTaskRequest = async (job: AcpJob, memoToSign?: AcpMemo) => { 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 job.createRequirementPayableMemo( "Send me USDC to open position", MemoType.PAYABLE_REQUEST, new FareAmount( - Number((job.requirement as V2DemoOpenPositionPayload)?.amount), + openPositionPayload.amount, config.baseFare // Open position against ACP Base Currency: USDC ), job.providerAddress @@ -100,27 +101,32 @@ const handleTaskRequest = async (job: AcpJob, memoToSign?: AcpMemo) => { case JobName.CLOSE_POSITION: const wallet = getClientWallet(job.clientAddress); - const symbol = (job.requirement as V2DemoClosePositionPayload)?.symbol + 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 console.log(`${positionIsValid ? "Accepts" : "Rejects"} position closing request`, job.requirement); await memoToSign.sign(positionIsValid, `${positionIsValid ? "Accepts" : "Rejects"} position closing`); if (positionIsValid) { - return job.createRequirementMemo(`Close ${symbol} position as per requested.`); + return job.createRequirementMemo(`Please make payment to close ${symbol}`); } break; 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 job.createRequirementPayableMemo( "Send me USDC to swap to VIRTUAL", MemoType.PAYABLE_REQUEST, new FareAmount( - Number((job.requirement as V2DemoSwapTokenPayload)?.amount), + swapTokenPayload.amount, await Fare.fromContractAddress( // Constructing Fare for the token to swap from - (job.requirement as V2DemoSwapTokenPayload)?.fromContractAddress, - baseSepoliaAcpConfig + swapTokenPayload.fromContractAddress, + config ) ), job.providerAddress @@ -142,19 +148,21 @@ const handleTaskTransaction = async (job: AcpJob) => { switch (jobName) { case JobName.OPEN_POSITION: + const openPositionPayload = job.requirement as V2DemoOpenPositionPayload; adjustPosition( wallet, - (job.requirement as V2DemoOpenPositionPayload)?.symbol, - Number((job.requirement as V2DemoOpenPositionPayload)?.amount) + openPositionPayload.symbol, + openPositionPayload.amount ); console.log(wallet); - return job.deliver({ type: "message", value: "Opened position with hash 0x123..." }); + return job.deliver({ type: "message", value: "Opened position with txn 0x71c038a47fd90069f133e991c4f19093e37bef26ca5c78398b9c99687395a97a" }); case JobName.CLOSE_POSITION: - const closingAmount = closePosition(wallet, (job.requirement as V2DemoClosePositionPayload)?.symbol) || 0; + const closePositionPayload = job.requirement as V2DemoClosePositionPayload; + const closingAmount = closePosition(wallet, closePositionPayload.symbol) || 0; console.log(wallet); await job.createRequirementPayableMemo( - `Close ${(job.requirement as V2DemoClosePositionPayload)?.symbol} position as per requested`, + `Close ${closePositionPayload.symbol} position as per requested`, MemoType.PAYABLE_TRANSFER_ESCROW, new FareAmount( closingAmount, @@ -162,7 +170,7 @@ const handleTaskTransaction = async (job: AcpJob) => { ), job.clientAddress, ) - return job.deliver({ type: "message", value: "Closed position with hash 0x123..." }); + return job.deliver({ type: "message", value: "Closed position with txn hash 0x0f60a30d66f1f3d21bad63e4e53e59d94ae286104fe8ea98f28425821edbca1b" }); case JobName.SWAP_TOKEN: await job.createRequirementPayableMemo( @@ -172,12 +180,12 @@ const handleTaskTransaction = async (job: AcpJob) => { 1, await Fare.fromContractAddress( // Constructing Fare for the token to swap to (job.requirement as V2DemoSwapTokenPayload)?.toContractAddress, - baseSepoliaAcpConfig + config ) ), job.clientAddress, ) - return job.deliver({ type: "message", value: "Swapped token with hash 0x123..." }); + return job.deliver({ type: "message", value: "Swapped token with txn hash 0x89996a3858db6708a3041b17b701637977f9548284afa1a4ef0a02ab04aa04ba" }); default: console.warn("[handleTaskTransaction] Unsupported job name", { jobId, jobName }); @@ -199,7 +207,7 @@ function closePosition(wallet: IClientWallet, symbol: string): number | undefine async function main() { new AcpClient({ - acpContractClient: await AcpContractClient.build( + acpContractClient: await AcpContractClientV2.build( WHITELISTED_WALLET_PRIVATE_KEY, SELLER_ENTITY_ID, SELLER_AGENT_WALLET_ADDRESS, diff --git a/examples/acp-base/polling-mode/buyer.ts b/examples/acp-base/polling-mode/buyer.ts index ecad902..0bb3c8d 100644 --- a/examples/acp-base/polling-mode/buyer.ts +++ b/examples/acp-base/polling-mode/buyer.ts @@ -1,5 +1,5 @@ import AcpClient, { - AcpContractClient, + AcpContractClientV2, AcpJobPhases, AcpGraduationStatus, AcpOnlineStatus, @@ -21,7 +21,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, diff --git a/examples/acp-base/polling-mode/seller.ts b/examples/acp-base/polling-mode/seller.ts index 563c4fd..95dad4f 100644 --- a/examples/acp-base/polling-mode/seller.ts +++ b/examples/acp-base/polling-mode/seller.ts @@ -1,5 +1,5 @@ import AcpClient, { - AcpContractClient, + AcpContractClientV2, AcpJobPhases, } from "@virtuals-protocol/acp-node"; import { @@ -18,7 +18,7 @@ async function sleep(ms: number) { 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, diff --git a/examples/acp-base/self-evaluation-v2/buyer.ts b/examples/acp-base/self-evaluation-v2/buyer.ts index 8445bd5..7c9c0a3 100644 --- a/examples/acp-base/self-evaluation-v2/buyer.ts +++ b/examples/acp-base/self-evaluation-v2/buyer.ts @@ -6,7 +6,7 @@ import AcpClient, { AcpAgentSort, AcpGraduationStatus, AcpOnlineStatus, - baseSepoliaAcpConfig + baseSepoliaAcpConfigV2 } from "../../../src"; import { BUYER_AGENT_WALLET_ADDRESS, @@ -20,7 +20,7 @@ async function buyer() { WHITELISTED_WALLET_PRIVATE_KEY, BUYER_ENTITY_ID, BUYER_AGENT_WALLET_ADDRESS, - baseSepoliaAcpConfig + baseSepoliaAcpConfigV2 ), onNewTask: async (job: AcpJob, memoToSign?: AcpMemo) => { if ( diff --git a/examples/acp-base/self-evaluation-v2/seller.ts b/examples/acp-base/self-evaluation-v2/seller.ts index 0358994..b5a1707 100644 --- a/examples/acp-base/self-evaluation-v2/seller.ts +++ b/examples/acp-base/self-evaluation-v2/seller.ts @@ -3,7 +3,7 @@ import AcpClient, { AcpJob, AcpJobPhases, AcpMemo, - baseSepoliaAcpConfig + baseSepoliaAcpConfigV2 } from '../../../src'; import { SELLER_AGENT_WALLET_ADDRESS, @@ -17,7 +17,7 @@ async function seller() { WHITELISTED_WALLET_PRIVATE_KEY, SELLER_ENTITY_ID, SELLER_AGENT_WALLET_ADDRESS, - baseSepoliaAcpConfig + baseSepoliaAcpConfigV2 ), onNewTask: async (job: AcpJob, memoToSign?: AcpMemo) => { if ( diff --git a/src/index.ts b/src/index.ts index 54d7141..998bafa 100644 --- a/src/index.ts +++ b/src/index.ts @@ -26,6 +26,7 @@ import { AcpContractConfig, baseAcpConfig, baseSepoliaAcpConfig, + baseSepoliaAcpConfigV2, } from "./configs/acpConfigs"; import { ethFare, Fare, FareAmount, FareBigInt, wethFare } from "./acpFare"; import AcpError from "./acpError"; @@ -45,6 +46,7 @@ export { wethFare, ethFare, baseSepoliaAcpConfig, + baseSepoliaAcpConfigV2, baseAcpConfig, AcpJobPhases, MemoType, From 0208249aecc4c384f24d5c0018633f8d8e269f0d Mon Sep 17 00:00:00 2001 From: Zuhwa Date: Sat, 4 Oct 2025 00:52:24 +0800 Subject: [PATCH 30/41] fix create payable memo --- src/contractClients/acpContractClient.ts | 43 +++++++++++++++++++- src/contractClients/baseAcpContractClient.ts | 6 ++- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/contractClients/acpContractClient.ts b/src/contractClients/acpContractClient.ts index 77971d1..f6c0213 100644 --- a/src/contractClients/acpContractClient.ts +++ b/src/contractClients/acpContractClient.ts @@ -7,7 +7,11 @@ import { import { encodeFunctionData, fromHex } from "viem"; import { AcpContractConfig, baseAcpConfig } from "../configs/acpConfigs"; import AcpError from "../acpError"; -import BaseAcpContractClient from "./baseAcpContractClient"; +import BaseAcpContractClient, { + AcpJobPhases, + FeeType, + MemoType, +} from "./baseAcpContractClient"; class AcpContractClient extends BaseAcpContractClient { protected MAX_RETRIES = 3; @@ -186,6 +190,43 @@ class AcpContractClient extends BaseAcpContractClient { } } + 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, evaluatorAddress: Address, diff --git a/src/contractClients/baseAcpContractClient.ts b/src/contractClients/baseAcpContractClient.ts index 6fcc0fb..f6aadc3 100644 --- a/src/contractClients/baseAcpContractClient.ts +++ b/src/contractClients/baseAcpContractClient.ts @@ -154,7 +154,8 @@ abstract class BaseAcpContractClient { nextPhase: AcpJobPhases, type: MemoType.PAYABLE_REQUEST | MemoType.PAYABLE_TRANSFER_ESCROW, expiredAt: Date, - token: Address = this.config.baseFare.contractAddress + token: Address = this.config.baseFare.contractAddress, + secured: boolean = true ) { try { const data = encodeFunctionData({ @@ -169,8 +170,9 @@ abstract class BaseAcpContractClient { feeAmountBaseUnit, feeType, type, - nextPhase, Math.floor(expiredAt.getTime() / 1000), + secured, + nextPhase, ], }); From 59a500c4288521fbb77b9d6afcb295fa776de8b0 Mon Sep 17 00:00:00 2001 From: Ang Weoy Yang Date: Sat, 4 Oct 2025 23:33:25 +0800 Subject: [PATCH 31/41] chore: upgrade self eval buyer to contract client v2 --- examples/acp-base/self-evaluation-v2/buyer.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/acp-base/self-evaluation-v2/buyer.ts b/examples/acp-base/self-evaluation-v2/buyer.ts index 7c9c0a3..6fff56c 100644 --- a/examples/acp-base/self-evaluation-v2/buyer.ts +++ b/examples/acp-base/self-evaluation-v2/buyer.ts @@ -1,5 +1,5 @@ import AcpClient, { - AcpContractClient, + AcpContractClientV2, AcpJobPhases, AcpJob, AcpMemo, @@ -16,7 +16,7 @@ import { 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, @@ -52,7 +52,7 @@ async function buyer() { // Browse available agents based on a keyword const relevantAgents = await acpClient.browseAgents( - "", + "Vrt Test", { sort_by: [AcpAgentSort.SUCCESSFUL_JOB_COUNT], top_k: 5, From 493339e20df5ce5e81ebd0959cf581d892e23f30 Mon Sep 17 00:00:00 2001 From: Ang Weoy Yang Date: Tue, 7 Oct 2025 11:41:42 +0800 Subject: [PATCH 32/41] docs: update logging message --- examples/acp-base/funds-v2/trading/seller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/acp-base/funds-v2/trading/seller.ts b/examples/acp-base/funds-v2/trading/seller.ts index 0fecd4e..dd2bb7e 100644 --- a/examples/acp-base/funds-v2/trading/seller.ts +++ b/examples/acp-base/funds-v2/trading/seller.ts @@ -120,7 +120,7 @@ const handleTaskRequest = async (job: AcpJob, memoToSign?: AcpMemo) => { const swapTokenPayload = job.requirement as V2DemoSwapTokenPayload; return job.createRequirementPayableMemo( - "Send me USDC to swap to VIRTUAL", + `Send me ${swapTokenPayload.fromSymbol} to swap to ${swapTokenPayload.toSymbol}`, MemoType.PAYABLE_REQUEST, new FareAmount( swapTokenPayload.amount, From c108d858cec2ff157cc5cf359a923dc30953b44b Mon Sep 17 00:00:00 2001 From: Zuhwa Date: Tue, 7 Oct 2025 12:07:45 +0800 Subject: [PATCH 33/41] imporve get job id reliability --- src/acpClient.ts | 1 + src/acpJobOffering.ts | 3 +- src/aibs/jobManagerAbi.ts | 857 +++++++++++++++++++ src/contractClients/acpContractClient.ts | 51 +- src/contractClients/acpContractClientV2.ts | 43 +- src/contractClients/baseAcpContractClient.ts | 41 +- 6 files changed, 968 insertions(+), 28 deletions(-) create mode 100644 src/aibs/jobManagerAbi.ts diff --git a/src/acpClient.ts b/src/acpClient.ts index 4827542..e5e5b1a 100644 --- a/src/acpClient.ts +++ b/src/acpClient.ts @@ -350,6 +350,7 @@ class AcpClient { ) : await this.acpContractClient.createJobWithAccount( account.id, + providerAddress, evaluatorAddress || this.walletAddress, fareAmount.amount, fareAmount.fare.contractAddress, diff --git a/src/acpJobOffering.ts b/src/acpJobOffering.ts index 38c635d..8a013ed 100644 --- a/src/acpJobOffering.ts +++ b/src/acpJobOffering.ts @@ -40,7 +40,7 @@ class AcpJobOffering { const finalServiceRequirement: Record = { name: this.name, requirement: serviceRequirement, - } + }; const fareAmount = new FareAmount( this.price, @@ -68,6 +68,7 @@ class AcpJobOffering { ) : await this.acpContractClient.createJobWithAccount( account.id, + this.providerAddress, evaluatorAddress || zeroAddress, fareAmount.amount, fareAmount.fare.contractAddress, diff --git a/src/aibs/jobManagerAbi.ts b/src/aibs/jobManagerAbi.ts new file mode 100644 index 0000000..8a8516f --- /dev/null +++ b/src/aibs/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/contractClients/acpContractClient.ts b/src/contractClients/acpContractClient.ts index f6c0213..8c32c4b 100644 --- a/src/contractClients/acpContractClient.ts +++ b/src/contractClients/acpContractClient.ts @@ -4,7 +4,7 @@ import { ModularAccountV2Client, createModularAccountV2Client, } from "@account-kit/smart-contracts"; -import { encodeFunctionData, fromHex } from "viem"; +import { decodeEventLog, encodeFunctionData, fromHex } from "viem"; import { AcpContractConfig, baseAcpConfig } from "../configs/acpConfigs"; import AcpError from "../acpError"; import BaseAcpContractClient, { @@ -136,28 +136,50 @@ class AcpContractClient extends BaseAcpContractClient { throw new AcpError(`Failed to send user operation`, finalError); } - async getJobId(hash: Address) { + 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.find( - (log: any) => - log.address.toLowerCase() === this.contractAddress.toLowerCase() - ) as any; + 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 (!contractLogs) { - throw new AcpError("Failed to get contract logs"); + if (!createdJobEvent) { + throw new AcpError("Failed to find created job event"); } - return fromHex(contractLogs.data, "number"); + return Number(createdJobEvent.args.jobId); } async createJob( - providerAddress: string, - evaluatorAddress: string, + providerAddress: Address, + evaluatorAddress: Address, expireAt: Date, paymentTokenAddress: Address, budgetBaseUnit: bigint, @@ -176,7 +198,11 @@ class AcpContractClient extends BaseAcpContractClient { const hash = await this.handleOperation(data, this.contractAddress); - const jobId = await this.getJobId(hash); + const jobId = await this.getJobId( + hash, + this.agentWalletAddress, + providerAddress + ); await this.setBudgetWithPaymentToken( jobId, @@ -229,6 +255,7 @@ class AcpContractClient extends BaseAcpContractClient { async createJobWithAccount( accountId: number, + providerAddress: Address, evaluatorAddress: Address, budgetBaseUnit: bigint, paymentTokenAddress: Address, diff --git a/src/contractClients/acpContractClientV2.ts b/src/contractClients/acpContractClientV2.ts index ad76b21..f43762d 100644 --- a/src/contractClients/acpContractClientV2.ts +++ b/src/contractClients/acpContractClientV2.ts @@ -4,10 +4,11 @@ import { ModularAccountV2Client, createModularAccountV2Client, } from "@account-kit/smart-contracts"; -import { createPublicClient, fromHex, http } from "viem"; +import { createPublicClient, decodeEventLog, fromHex, http } from "viem"; import { AcpContractConfig, baseAcpConfig } from "../configs/acpConfigs"; import AcpError from "../acpError"; import BaseAcpContractClient from "./baseAcpContractClient"; +import JOB_MANAGER_ABI from "../aibs/jobManagerAbi"; class AcpContractClientV2 extends BaseAcpContractClient { private MAX_RETRIES = 3; @@ -175,23 +176,47 @@ class AcpContractClientV2 extends BaseAcpContractClient { throw new AcpError(`Failed to send user operation`, finalError); } - async getJobId(hash: Address) { + 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.find( - (log: any) => - log.address.toLowerCase() === this.jobManagerAddress.toLowerCase() - ) as any; + 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 (!contractLogs) { - throw new AcpError("Failed to get contract logs"); + if (!createdJobEvent) { + throw new AcpError("Failed to find created job event"); } - return fromHex(contractLogs.topics[1], "number"); + return Number(createdJobEvent.args.jobId); } } diff --git a/src/contractClients/baseAcpContractClient.ts b/src/contractClients/baseAcpContractClient.ts index f6aadc3..4946130 100644 --- a/src/contractClients/baseAcpContractClient.ts +++ b/src/contractClients/baseAcpContractClient.ts @@ -1,4 +1,13 @@ -import { Address, Chain, encodeFunctionData, erc20Abi } from "viem"; +import { + AbiEvent, + Address, + Chain, + encodeFunctionData, + erc20Abi, + keccak256, + toEventSignature, + toHex, +} from "viem"; import { AcpContractConfig, baseAcpConfig } from "../configs/acpConfigs"; import ACP_V2_ABI from "../aibs/acpAbiV2"; import ACP_ABI from "../aibs/acpAbi"; @@ -43,6 +52,7 @@ 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, @@ -51,6 +61,12 @@ abstract class BaseAcpContractClient { 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( @@ -59,7 +75,11 @@ abstract class BaseAcpContractClient { value?: bigint ): Promise
; - abstract getJobId(hash: Address): Promise; + abstract getJobId( + hash: Address, + clientAddress: Address, + providerAddress: Address + ): Promise; get walletAddress() { return this.agentWalletAddress; @@ -67,6 +87,7 @@ abstract class BaseAcpContractClient { async createJobWithAccount( accountId: number, + providerAddress: Address, evaluatorAddress: Address, budgetBaseUnit: bigint, paymentTokenAddress: Address, @@ -87,7 +108,11 @@ abstract class BaseAcpContractClient { const hash = await this.handleOperation(data, this.contractAddress); - const jobId = await this.getJobId(hash); + const jobId = await this.getJobId( + hash, + this.agentWalletAddress, + providerAddress + ); return { txHash: hash, jobId: jobId }; } catch (error) { @@ -96,8 +121,8 @@ abstract class BaseAcpContractClient { } async createJob( - providerAddress: string, - evaluatorAddress: string, + providerAddress: Address, + evaluatorAddress: Address, expireAt: Date, paymentTokenAddress: Address, budgetBaseUnit: bigint, @@ -119,7 +144,11 @@ abstract class BaseAcpContractClient { const hash = await this.handleOperation(data, this.contractAddress); - const jobId = await this.getJobId(hash); + const jobId = await this.getJobId( + hash, + this.agentWalletAddress, + providerAddress + ); return { txHash: hash, jobId: jobId }; } catch (error) { From f7e478b8fdeb0bfc87f08a803f24aea08ddefe9c Mon Sep 17 00:00:00 2001 From: Ang Weoy Yang Date: Wed, 8 Oct 2025 18:17:24 +0800 Subject: [PATCH 34/41] docs: update example code --- README.md | 3 +-- .../acp-base/external-evaluation/README.md | 1 - .../acp-base/external-evaluation/buyer.ts | 3 +-- examples/acp-base/funds-v2/trading/buyer.ts | 19 ++++--------------- examples/acp-base/funds/README.md | 1 - examples/acp-base/funds/buyer.ts | 1 - examples/acp-base/polling-mode/buyer.ts | 6 ++++-- examples/acp-base/self-evaluation-v2/buyer.ts | 2 +- examples/acp-base/self-evaluation/README.md | 1 - examples/acp-base/self-evaluation/buyer.ts | 3 +-- .../langchain/buyerLangchain.ts | 3 +-- 11 files changed, 13 insertions(+), 30 deletions(-) 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/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/trading/buyer.ts b/examples/acp-base/funds-v2/trading/buyer.ts index 53e77a4..00db5eb 100644 --- a/examples/acp-base/funds-v2/trading/buyer.ts +++ b/examples/acp-base/funds-v2/trading/buyer.ts @@ -52,10 +52,12 @@ async function main() { onNewTask: async (job: AcpJob, memoToSign?: AcpMemo) => { const { id: jobId, phase: jobPhase } = job; if (!memoToSign) { - console.log("[onNewTask] No memo to sign", { jobId }); - if (job.phase === AcpJobPhases.REJECTED) { + 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; @@ -86,19 +88,6 @@ async function main() { console.log("[onNewTask] Funds transfer memo signed", { jobId }); } } - }, - onEvaluate: async (job: AcpJob) => { - console.log( - "[onEvaluate] Evaluation function called", - { - jobId: job.id, - requirement: job.requirement, - deliverable: job.deliverable, - } - ); - await job.evaluate(true, "job auto-evaluated"); - console.log(`[onEvaluate] Job ${job.id} evaluated`); - currentJobId = null; } }); 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 0bb3c8d..edc0a8f 100644 --- a/examples/acp-base/polling-mode/buyer.ts +++ b/examples/acp-base/polling-mode/buyer.ts @@ -3,6 +3,7 @@ import AcpClient, { AcpJobPhases, AcpGraduationStatus, AcpOnlineStatus, + AcpAgentSort } from "@virtuals-protocol/acp-node"; import { 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); diff --git a/examples/acp-base/self-evaluation-v2/buyer.ts b/examples/acp-base/self-evaluation-v2/buyer.ts index 6fff56c..04fc255 100644 --- a/examples/acp-base/self-evaluation-v2/buyer.ts +++ b/examples/acp-base/self-evaluation-v2/buyer.ts @@ -52,7 +52,7 @@ async function buyer() { // Browse available agents based on a keyword const relevantAgents = await acpClient.browseAgents( - "Vrt Test", + "", { sort_by: [AcpAgentSort.SUCCESSFUL_JOB_COUNT], top_k: 5, 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..f7bf713 100644 --- a/examples/agent-frameworks/langchain/buyerLangchain.ts +++ b/examples/agent-frameworks/langchain/buyerLangchain.ts @@ -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, From f4af6a7e79926b52ef991754f295dc23451cdb2b Mon Sep 17 00:00:00 2001 From: Ang Weoy Yang Date: Thu, 9 Oct 2025 10:45:07 +0800 Subject: [PATCH 35/41] docs: remove onEvaluate for self eval examples --- examples/acp-base/self-evaluation-v2/buyer.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/examples/acp-base/self-evaluation-v2/buyer.ts b/examples/acp-base/self-evaluation-v2/buyer.ts index 04fc255..2be2ebe 100644 --- a/examples/acp-base/self-evaluation-v2/buyer.ts +++ b/examples/acp-base/self-evaluation-v2/buyer.ts @@ -42,12 +42,7 @@ async function buyer() { } else if (job.phase === AcpJobPhases.REJECTED) { console.log(`Job ${job.id} rejected`); } - }, - onEvaluate: async (job: AcpJob) => { - console.log("Evaluation function called", job); - await job.evaluate(true, "Self-evaluated and approved"); - console.log(`Job ${job.id} evaluated`); - }, + } }); // Browse available agents based on a keyword From 6ef9bfa8996b14651e4f1106f62f64a7d615359f Mon Sep 17 00:00:00 2001 From: Ang Weoy Yang Date: Thu, 9 Oct 2025 11:04:59 +0800 Subject: [PATCH 36/41] docs: update v2 examples log --- .../acp-base/external-evaluation-v2/buyer.ts | 4 +-- .../acp-base/external-evaluation-v2/seller.ts | 31 +++++++++++-------- examples/acp-base/funds-v2/trading/buyer.ts | 4 +-- examples/acp-base/polling-mode/buyer.ts | 7 +++-- examples/acp-base/polling-mode/seller.ts | 26 +++++++++++----- examples/acp-base/self-evaluation-v2/buyer.ts | 4 +-- .../acp-base/self-evaluation-v2/seller.ts | 31 +++++++++++-------- .../langchain/buyerLangchain.ts | 2 +- 8 files changed, 65 insertions(+), 44 deletions(-) diff --git a/examples/acp-base/external-evaluation-v2/buyer.ts b/examples/acp-base/external-evaluation-v2/buyer.ts index f147b0d..e03c606 100644 --- a/examples/acp-base/external-evaluation-v2/buyer.ts +++ b/examples/acp-base/external-evaluation-v2/buyer.ts @@ -28,7 +28,7 @@ async function buyer() { job.phase === AcpJobPhases.NEGOTIATION && memoToSign?.nextPhase === AcpJobPhases.TRANSACTION ) { - console.log("Paying job", job); + console.log(`Paying for job ${job.id}`); await job.payAndAcceptRequirement(); console.log(`Job ${job.id} paid`); } else if ( @@ -39,7 +39,7 @@ async function buyer() { 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`); + console.log(`Job ${job.id} completed, received deliverable:`, job.deliverable); } else if (job.phase === AcpJobPhases.REJECTED) { console.log(`Job ${job.id} rejected`); } diff --git a/examples/acp-base/external-evaluation-v2/seller.ts b/examples/acp-base/external-evaluation-v2/seller.ts index 0d025ed..0f0c426 100644 --- a/examples/acp-base/external-evaluation-v2/seller.ts +++ b/examples/acp-base/external-evaluation-v2/seller.ts @@ -1,9 +1,10 @@ import AcpClient, { AcpContractClientV2, - AcpJobPhases, + AcpJobPhases, AcpJob, AcpMemo, - baseSepoliaAcpConfigV2 + baseSepoliaAcpConfigV2, + IDeliverable } from '../../../src'; import { SELLER_AGENT_WALLET_ADDRESS, @@ -11,6 +12,8 @@ import { WHITELISTED_WALLET_PRIVATE_KEY } from "./env"; +const REJECT_JOB = false + async function seller() { new AcpClient({ acpContractClient: await AcpContractClientV2.build( @@ -31,18 +34,20 @@ async function seller() { job.phase === AcpJobPhases.TRANSACTION && memoToSign?.nextPhase === AcpJobPhases.EVALUATION ) { - // // to cater cases where agent decide to reject job after payment has been made - // console.log("Rejecting job", job) - // await job.reject("Job requirement does not meet agent capability"); - // console.log(`Job ${job.id} rejected`); + // to cater cases where agent decide to reject job after payment has been made + if (REJECT_JOB) { // conditional check for job rejection logic + console.log("Rejecting job", job) + await job.reject("Job requirement does not meet agent capability"); + console.log(`Job ${job.id} rejected`); + return; + } - console.log("Delivering job", job); - await job.deliver( - { - type: "url", - value: "https://example.com", - } - ); + 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`); } } diff --git a/examples/acp-base/funds-v2/trading/buyer.ts b/examples/acp-base/funds-v2/trading/buyer.ts index 00db5eb..64ff451 100644 --- a/examples/acp-base/funds-v2/trading/buyer.ts +++ b/examples/acp-base/funds-v2/trading/buyer.ts @@ -67,10 +67,10 @@ async function main() { jobPhase === AcpJobPhases.NEGOTIATION && memoToSign.nextPhase === AcpJobPhases.TRANSACTION ) { - console.log("[onNewTask] Paying job", jobId); + console.log(`[onNewTask] Paying for job ${jobId}`); await job.payAndAcceptRequirement(); currentJobId = jobId; - console.log("[onNewTask] Job paid", jobId); + console.log(`[onNewTask] Job ${jobId} paid`); } else if ( jobPhase === AcpJobPhases.TRANSACTION ) { diff --git a/examples/acp-base/polling-mode/buyer.ts b/examples/acp-base/polling-mode/buyer.ts index edc0a8f..7d7d3ea 100644 --- a/examples/acp-base/polling-mode/buyer.ts +++ b/examples/acp-base/polling-mode/buyer.ts @@ -82,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); + console.log(`Paying for job ${jobId}`); await job.payAndAcceptRequirement(); + console.log(`Job ${jobId} paid`) } } } else if (job.phase === AcpJobPhases.REQUEST) { @@ -93,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 95dad4f..098dd0f 100644 --- a/examples/acp-base/polling-mode/seller.ts +++ b/examples/acp-base/polling-mode/seller.ts @@ -1,6 +1,7 @@ import AcpClient, { AcpContractClientV2, AcpJobPhases, + IDeliverable } from "@virtuals-protocol/acp-node"; import { SELLER_AGENT_WALLET_ADDRESS, @@ -12,6 +13,8 @@ import { const POLL_INTERVAL_MS = 20000; // 20 seconds // -------------------------------------------------- +const REJECT_JOB = false; + async function sleep(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); } @@ -69,14 +72,21 @@ async function seller() { // 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 + console.log("Rejecting job", job) + await job.reject("Job requirement does not meet agent capability"); + 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/buyer.ts b/examples/acp-base/self-evaluation-v2/buyer.ts index 2be2ebe..6231708 100644 --- a/examples/acp-base/self-evaluation-v2/buyer.ts +++ b/examples/acp-base/self-evaluation-v2/buyer.ts @@ -27,7 +27,7 @@ async function buyer() { job.phase === AcpJobPhases.NEGOTIATION && memoToSign?.nextPhase === AcpJobPhases.TRANSACTION ) { - console.log("Paying job", job); + console.log(`Paying for job ${job.id}`); await job.payAndAcceptRequirement(); console.log(`Job ${job.id} paid`); } else if ( @@ -38,7 +38,7 @@ async function buyer() { 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`); + console.log(`Job ${job.id} completed, received deliverable:`, job.deliverable); } else if (job.phase === AcpJobPhases.REJECTED) { console.log(`Job ${job.id} rejected`); } diff --git a/examples/acp-base/self-evaluation-v2/seller.ts b/examples/acp-base/self-evaluation-v2/seller.ts index b5a1707..d392a7a 100644 --- a/examples/acp-base/self-evaluation-v2/seller.ts +++ b/examples/acp-base/self-evaluation-v2/seller.ts @@ -3,7 +3,8 @@ import AcpClient, { AcpJob, AcpJobPhases, AcpMemo, - baseSepoliaAcpConfigV2 + baseSepoliaAcpConfigV2, + IDeliverable } from '../../../src'; import { SELLER_AGENT_WALLET_ADDRESS, @@ -11,6 +12,8 @@ import { WHITELISTED_WALLET_PRIVATE_KEY } from "./env"; +const REJECT_JOB = false; + async function seller() { new AcpClient({ acpContractClient: await AcpContractClient.build( @@ -24,25 +27,27 @@ async function seller() { job.phase === AcpJobPhases.REQUEST && memoToSign?.nextPhase === AcpJobPhases.NEGOTIATION ) { - console.log("Responding to job", job); + console.log(`Responding to job ${job.id} with requirement`, job.requirement); await job.respond(true); console.log(`Job ${job.id} responded`); } else if ( job.phase === AcpJobPhases.TRANSACTION && memoToSign?.nextPhase === AcpJobPhases.EVALUATION ) { - // // to cater cases where agent decide to reject job after payment has been made - // console.log("Rejecting job", job) - // await job.reject("Job requirement does not meet agent capability"); - // console.log(`Job ${job.id} rejected`); + // to cater cases where agent decide to reject job after payment has been made + if (REJECT_JOB) { // conditional check for job rejection logic + console.log("Rejecting job", job) + await job.reject("Job requirement does not meet agent capability"); + console.log(`Job ${job.id} rejected`); + return; + } - console.log("Delivering job", job); - await job.deliver( - { - type: "url", - value: "https://example.com", - } - ); + 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`); } }, diff --git a/examples/agent-frameworks/langchain/buyerLangchain.ts b/examples/agent-frameworks/langchain/buyerLangchain.ts index f7bf713..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); From d5283b1508db76f5c374f423a23719a4bd3989e3 Mon Sep 17 00:00:00 2001 From: Ang Weoy Yang Date: Thu, 9 Oct 2025 11:39:48 +0800 Subject: [PATCH 37/41] feat: enhance job rejection logic --- .../acp-base/external-evaluation-v2/buyer.ts | 2 +- .../acp-base/external-evaluation-v2/seller.ts | 12 ++++--- examples/acp-base/polling-mode/seller.ts | 10 ++++-- examples/acp-base/self-evaluation-v2/buyer.ts | 2 +- .../acp-base/self-evaluation-v2/seller.ts | 10 +++--- src/acpJob.ts | 32 ++++++++++++------- 6 files changed, 43 insertions(+), 25 deletions(-) diff --git a/examples/acp-base/external-evaluation-v2/buyer.ts b/examples/acp-base/external-evaluation-v2/buyer.ts index e03c606..633af0f 100644 --- a/examples/acp-base/external-evaluation-v2/buyer.ts +++ b/examples/acp-base/external-evaluation-v2/buyer.ts @@ -36,7 +36,7 @@ async function buyer() { memoToSign?.nextPhase === AcpJobPhases.REJECTED ) { console.log("Signing job rejection memo", job); - await memoToSign?.sign(true, "Accepts job rejection") + 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); diff --git a/examples/acp-base/external-evaluation-v2/seller.ts b/examples/acp-base/external-evaluation-v2/seller.ts index 0f0c426..d6b7ecc 100644 --- a/examples/acp-base/external-evaluation-v2/seller.ts +++ b/examples/acp-base/external-evaluation-v2/seller.ts @@ -27,17 +27,19 @@ async function seller() { job.phase === AcpJobPhases.REQUEST && job.memos.find((m) => m.nextPhase === AcpJobPhases.NEGOTIATION) ) { - console.log("Responding to job", job); - await job.respond(true); - console.log(`Job ${job.id} responded`); + 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 - console.log("Rejecting job", job) - await job.reject("Job requirement does not meet agent capability"); + 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; } diff --git a/examples/acp-base/polling-mode/seller.ts b/examples/acp-base/polling-mode/seller.ts index 098dd0f..7387422 100644 --- a/examples/acp-base/polling-mode/seller.ts +++ b/examples/acp-base/polling-mode/seller.ts @@ -65,7 +65,10 @@ 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; } @@ -74,8 +77,9 @@ async function seller() { // Buyer has paid, job is in TRANSACTION. Seller needs to deliver. // to cater cases where agent decide to reject job after payment has been made if (REJECT_JOB) { // conditional check for job rejection logic - console.log("Rejecting job", job) - await job.reject("Job requirement does not meet agent capability"); + 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; } diff --git a/examples/acp-base/self-evaluation-v2/buyer.ts b/examples/acp-base/self-evaluation-v2/buyer.ts index 6231708..1f50d61 100644 --- a/examples/acp-base/self-evaluation-v2/buyer.ts +++ b/examples/acp-base/self-evaluation-v2/buyer.ts @@ -34,7 +34,7 @@ async function buyer() { 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}`); await memoToSign?.sign(true, "Accepts job rejection") console.log(`Job ${job.id} rejection memo signed`); } else if (job.phase === AcpJobPhases.COMPLETED) { diff --git a/examples/acp-base/self-evaluation-v2/seller.ts b/examples/acp-base/self-evaluation-v2/seller.ts index d392a7a..74b28f2 100644 --- a/examples/acp-base/self-evaluation-v2/seller.ts +++ b/examples/acp-base/self-evaluation-v2/seller.ts @@ -27,17 +27,19 @@ async function seller() { 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(true); - console.log(`Job ${job.id} responded`); + 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 - console.log("Rejecting job", job) - await job.reject("Job requirement does not meet agent capability"); + 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; } diff --git a/src/acpJob.ts b/src/acpJob.ts index 8e18080..08930c9 100644 --- a/src/acpJob.ts +++ b/src/acpJob.ts @@ -110,7 +110,7 @@ class AcpJob { this.id, content, MemoType.MESSAGE, - false, + true, AcpJobPhases.TRANSACTION ); } @@ -222,16 +222,26 @@ class AcpJob { } async reject(reason?: string) { - if (this.latestMemo?.nextPhase !== AcpJobPhases.NEGOTIATION) { - throw new AcpError("No negotiation memo found"); + 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 + ); } - const memo = this.latestMemo; - - return await this.acpContractClient.signMemo( - memo.id, - false, - `Job ${this.id} rejected. ${reason || ""}` + return await this.acpContractClient.createMemo( + this.id, + memoContent, + MemoType.MESSAGE, + true, + AcpJobPhases.REJECTED ); } @@ -259,7 +269,7 @@ class AcpJob { await memo.sign(accept, reason); } - async pay(amount: number, reason?: string) { + async pay(reason?: string) { const memo = this.memos.find( (m) => m.nextPhase === AcpJobPhases.TRANSACTION ); @@ -270,7 +280,7 @@ class AcpJob { return await this.acpClient.payJob( this.id, - this.baseFare.formatAmount(amount), + this.baseFare.formatAmount(this.price), memo.id, reason ); From 3a2c3f5a7222ed07e94f56c70cd55089d4bd3142 Mon Sep 17 00:00:00 2001 From: Ang Weoy Yang Date: Thu, 9 Oct 2025 17:16:10 +0800 Subject: [PATCH 38/41] docs: improve examples logging --- examples/acp-base/funds-v2/trading/buyer.ts | 4 +- examples/acp-base/funds-v2/trading/seller.ts | 53 ++++++++++++++------ 2 files changed, 39 insertions(+), 18 deletions(-) diff --git a/examples/acp-base/funds-v2/trading/buyer.ts b/examples/acp-base/funds-v2/trading/buyer.ts index 64ff451..5b62e55 100644 --- a/examples/acp-base/funds-v2/trading/buyer.ts +++ b/examples/acp-base/funds-v2/trading/buyer.ts @@ -76,7 +76,7 @@ async function main() { ) { if (memoToSign.nextPhase === AcpJobPhases.REJECTED) { console.log("[onNewTask] Signing job rejection memo", { jobId, memoId }); - await memoToSign.sign(true, "Accepted job rejection"); + await memoToSign.sign(true, "Accepts job rejection"); console.log("[onNewTask] Rejection memo signed", { jobId }); currentJobId = null; } else if ( @@ -84,7 +84,7 @@ async function main() { memoToSign.type === MemoType.PAYABLE_TRANSFER_ESCROW ) { console.log("[onNewTask] Accepting funds transfer", { jobId, memoId }); - await memoToSign.sign(true, "Accepted funds transfer"); + await memoToSign.sign(true, "Accepts funds transfer"); console.log("[onNewTask] Funds transfer memo signed", { jobId }); } } diff --git a/examples/acp-base/funds-v2/trading/seller.ts b/examples/acp-base/funds-v2/trading/seller.ts index dd2bb7e..8e4e791 100644 --- a/examples/acp-base/funds-v2/trading/seller.ts +++ b/examples/acp-base/funds-v2/trading/seller.ts @@ -44,6 +44,10 @@ interface IClientWallet { const client: Record = {}; +async function sleep(seconds: number) { + return new Promise((resolve) => setTimeout(resolve, seconds * 1000)); +} + const getClientWallet = (address: Address): IClientWallet => { const hash = createHash("sha256").update(address).digest("hex"); const walletAddress = `0x${hash}` as Address; @@ -85,7 +89,7 @@ const handleTaskRequest = async (job: AcpJob, memoToSign?: AcpMemo) => { } switch (jobName) { - case JobName.OPEN_POSITION: + 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; @@ -98,8 +102,9 @@ const handleTaskRequest = async (job: AcpJob, memoToSign?: AcpMemo) => { ), job.providerAddress ); + } - case JobName.CLOSE_POSITION: + case JobName.CLOSE_POSITION: { const wallet = getClientWallet(job.clientAddress); const closePositionPayload = job.requirement as V2DemoClosePositionPayload; @@ -107,13 +112,13 @@ const handleTaskRequest = async (job: AcpJob, memoToSign?: AcpMemo) => { const position = wallet.positions.find((p) => p.symbol === symbol); const positionIsValid = !!position && position.amount > 0 console.log(`${positionIsValid ? "Accepts" : "Rejects"} position closing request`, job.requirement); - await memoToSign.sign(positionIsValid, `${positionIsValid ? "Accepts" : "Rejects"} position closing`); - if (positionIsValid) { - return job.createRequirementMemo(`Please make payment to close ${symbol}`); - } - break; + const response = positionIsValid + ? `Accepts position closing. Please make payment to close ${symbol} position.` + : "Rejects position closing. Position is invalid."; + return job.respond(positionIsValid, response); + } - case JobName.SWAP_TOKEN: + case JobName.SWAP_TOKEN: { console.log("Accepts token swapping request", job.requirement); await memoToSign.sign(true, "Accepts token swapping request"); @@ -131,6 +136,7 @@ const handleTaskRequest = async (job: AcpJob, memoToSign?: AcpMemo) => { ), job.providerAddress ); + } default: console.warn("[handleTaskRequest] Unsupported job name", { jobId, jobName }); @@ -147,7 +153,7 @@ const handleTaskTransaction = async (job: AcpJob) => { } switch (jobName) { - case JobName.OPEN_POSITION: + case JobName.OPEN_POSITION: { const openPositionPayload = job.requirement as V2DemoOpenPositionPayload; adjustPosition( wallet, @@ -155,9 +161,14 @@ const handleTaskTransaction = async (job: AcpJob) => { openPositionPayload.amount ); console.log(wallet); - return job.deliver({ type: "message", value: "Opened position with txn 0x71c038a47fd90069f133e991c4f19093e37bef26ca5c78398b9c99687395a97a" }); - - case JobName.CLOSE_POSITION: + await sleep(3); // TODO: remove this after eval phase fix is in + return job.deliver({ + type: "message", + value: "Opened position with txn 0x71c038a47fd90069f133e991c4f19093e37bef26ca5c78398b9c99687395a97a" + }); + } + + case JobName.CLOSE_POSITION: { const closePositionPayload = job.requirement as V2DemoClosePositionPayload; const closingAmount = closePosition(wallet, closePositionPayload.symbol) || 0; console.log(wallet); @@ -170,9 +181,14 @@ const handleTaskTransaction = async (job: AcpJob) => { ), job.clientAddress, ) - return job.deliver({ type: "message", value: "Closed position with txn hash 0x0f60a30d66f1f3d21bad63e4e53e59d94ae286104fe8ea98f28425821edbca1b" }); - - case JobName.SWAP_TOKEN: + await sleep(3); // TODO: remove this after eval phase fix is in + return job.deliver({ + type: "message", + value: "Closed position with txn hash 0x0f60a30d66f1f3d21bad63e4e53e59d94ae286104fe8ea98f28425821edbca1b" + }); + } + + case JobName.SWAP_TOKEN: { await job.createRequirementPayableMemo( `Return swapped token ${(job.requirement as V2DemoSwapTokenPayload)?.toSymbol}`, MemoType.PAYABLE_TRANSFER_ESCROW, @@ -185,7 +201,12 @@ const handleTaskTransaction = async (job: AcpJob) => { ), job.clientAddress, ) - return job.deliver({ type: "message", value: "Swapped token with txn hash 0x89996a3858db6708a3041b17b701637977f9548284afa1a4ef0a02ab04aa04ba" }); + await sleep(3); // TODO: remove this after eval phase fix is in + return job.deliver({ + type: "message", + value: "Swapped token with txn hash 0x89996a3858db6708a3041b17b701637977f9548284afa1a4ef0a02ab04aa04ba" + }); + } default: console.warn("[handleTaskTransaction] Unsupported job name", { jobId, jobName }); From 4a46b379624cd9f9e2045c54b6d6cc0bea388744 Mon Sep 17 00:00:00 2001 From: Zuhwa Date: Thu, 9 Oct 2025 18:12:25 +0800 Subject: [PATCH 39/41] allow payable transfer memo for requirement --- src/acpJob.ts | 10 ++++++++-- src/contractClients/baseAcpContractClient.ts | 5 ++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/acpJob.ts b/src/acpJob.ts index 08930c9..cc75e5f 100644 --- a/src/acpJob.ts +++ b/src/acpJob.ts @@ -117,12 +117,18 @@ class AcpJob { async createRequirementPayableMemo( content: string, - type: MemoType.PAYABLE_REQUEST | MemoType.PAYABLE_TRANSFER_ESCROW, + type: + | MemoType.PAYABLE_REQUEST + | MemoType.PAYABLE_TRANSFER_ESCROW + | MemoType.PAYABLE_TRANSFER, amount: FareAmountBase, recipient: Address, expiredAt: Date = new Date(Date.now() + 1000 * 60 * 5) // 5 minutes ) { - if (type === MemoType.PAYABLE_TRANSFER_ESCROW) { + if ( + type === MemoType.PAYABLE_TRANSFER_ESCROW || + type === MemoType.PAYABLE_TRANSFER + ) { await this.acpContractClient.approveAllowance( amount.amount, amount.fare.contractAddress diff --git a/src/contractClients/baseAcpContractClient.ts b/src/contractClients/baseAcpContractClient.ts index 4946130..ba80c8b 100644 --- a/src/contractClients/baseAcpContractClient.ts +++ b/src/contractClients/baseAcpContractClient.ts @@ -181,7 +181,10 @@ abstract class BaseAcpContractClient { feeAmountBaseUnit: bigint, feeType: FeeType, nextPhase: AcpJobPhases, - type: MemoType.PAYABLE_REQUEST | MemoType.PAYABLE_TRANSFER_ESCROW, + type: + | MemoType.PAYABLE_REQUEST + | MemoType.PAYABLE_TRANSFER_ESCROW + | MemoType.PAYABLE_TRANSFER, expiredAt: Date, token: Address = this.config.baseFare.contractAddress, secured: boolean = true From ff1b15eed494fde9fd0379960508b0d97c5c240e Mon Sep 17 00:00:00 2001 From: Zuhwa Date: Fri, 10 Oct 2025 10:34:05 +0800 Subject: [PATCH 40/41] rename abis folder --- src/{aibs => abis}/acpAbi.ts | 0 src/{aibs => abis}/acpAbiV2.ts | 0 src/{aibs => abis}/jobManagerAbi.ts | 0 src/{aibs => abis}/wethAbi.ts | 0 src/configs/acpConfigs.ts | 4 ++-- src/contractClients/acpContractClientV2.ts | 2 +- src/contractClients/baseAcpContractClient.ts | 6 +++--- src/index.ts | 2 +- 8 files changed, 7 insertions(+), 7 deletions(-) rename src/{aibs => abis}/acpAbi.ts (100%) rename src/{aibs => abis}/acpAbiV2.ts (100%) rename src/{aibs => abis}/jobManagerAbi.ts (100%) rename src/{aibs => abis}/wethAbi.ts (100%) diff --git a/src/aibs/acpAbi.ts b/src/abis/acpAbi.ts similarity index 100% rename from src/aibs/acpAbi.ts rename to src/abis/acpAbi.ts diff --git a/src/aibs/acpAbiV2.ts b/src/abis/acpAbiV2.ts similarity index 100% rename from src/aibs/acpAbiV2.ts rename to src/abis/acpAbiV2.ts diff --git a/src/aibs/jobManagerAbi.ts b/src/abis/jobManagerAbi.ts similarity index 100% rename from src/aibs/jobManagerAbi.ts rename to src/abis/jobManagerAbi.ts diff --git a/src/aibs/wethAbi.ts b/src/abis/wethAbi.ts similarity index 100% rename from src/aibs/wethAbi.ts rename to src/abis/wethAbi.ts diff --git a/src/configs/acpConfigs.ts b/src/configs/acpConfigs.ts index 8114413..8f4e086 100644 --- a/src/configs/acpConfigs.ts +++ b/src/configs/acpConfigs.ts @@ -1,8 +1,8 @@ import { Address } from "@aa-sdk/core"; import { baseSepolia, base } from "@account-kit/infra"; import { Fare } from "../acpFare"; -import ACP_ABI from "../aibs/acpAbi"; -import ACP_V2_ABI from "../aibs/acpAbiV2"; +import ACP_ABI from "../abis/acpAbi"; +import ACP_V2_ABI from "../abis/acpAbiV2"; class AcpContractConfig { constructor( diff --git a/src/contractClients/acpContractClientV2.ts b/src/contractClients/acpContractClientV2.ts index f43762d..a0fbe15 100644 --- a/src/contractClients/acpContractClientV2.ts +++ b/src/contractClients/acpContractClientV2.ts @@ -8,7 +8,7 @@ import { createPublicClient, decodeEventLog, fromHex, http } from "viem"; import { AcpContractConfig, baseAcpConfig } from "../configs/acpConfigs"; import AcpError from "../acpError"; import BaseAcpContractClient from "./baseAcpContractClient"; -import JOB_MANAGER_ABI from "../aibs/jobManagerAbi"; +import JOB_MANAGER_ABI from "../abis/jobManagerAbi"; class AcpContractClientV2 extends BaseAcpContractClient { private MAX_RETRIES = 3; diff --git a/src/contractClients/baseAcpContractClient.ts b/src/contractClients/baseAcpContractClient.ts index ba80c8b..3d874c5 100644 --- a/src/contractClients/baseAcpContractClient.ts +++ b/src/contractClients/baseAcpContractClient.ts @@ -9,10 +9,10 @@ import { toHex, } from "viem"; import { AcpContractConfig, baseAcpConfig } from "../configs/acpConfigs"; -import ACP_V2_ABI from "../aibs/acpAbiV2"; -import ACP_ABI from "../aibs/acpAbi"; +import ACP_V2_ABI from "../abis/acpAbiV2"; +import ACP_ABI from "../abis/acpAbi"; import AcpError from "../acpError"; -import WETH_ABI from "../aibs/wethAbi"; +import WETH_ABI from "../abis/wethAbi"; import { wethFare } from "../acpFare"; export enum MemoType { diff --git a/src/index.ts b/src/index.ts index 998bafa..b42faea 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -import ACP_ABI from "./aibs/acpAbi"; +import ACP_ABI from "./abis/acpAbi"; import AcpClient from "./acpClient"; import AcpContractClient from "./contractClients/acpContractClient"; import BaseAcpContractClient, { From c1b21a55e76cc7c635be9b0a827b44cb424de004 Mon Sep 17 00:00:00 2001 From: Ang Weoy Yang Date: Fri, 10 Oct 2025 14:36:26 +0800 Subject: [PATCH 41/41] fix: add /1000 to create job timestamp --- src/contractClients/baseAcpContractClient.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/contractClients/baseAcpContractClient.ts b/src/contractClients/baseAcpContractClient.ts index 3d874c5..61993d9 100644 --- a/src/contractClients/baseAcpContractClient.ts +++ b/src/contractClients/baseAcpContractClient.ts @@ -102,7 +102,7 @@ abstract class BaseAcpContractClient { evaluatorAddress, budgetBaseUnit, paymentTokenAddress, - expiredAt, + Math.floor(expiredAt.getTime() / 1000), ], }); @@ -123,7 +123,7 @@ abstract class BaseAcpContractClient { async createJob( providerAddress: Address, evaluatorAddress: Address, - expireAt: Date, + expiredAt: Date, paymentTokenAddress: Address, budgetBaseUnit: bigint, metadata: string @@ -135,7 +135,7 @@ abstract class BaseAcpContractClient { args: [ providerAddress, evaluatorAddress, - Math.floor(expireAt.getTime() / 1000), + Math.floor(expiredAt.getTime() / 1000), paymentTokenAddress, budgetBaseUnit, metadata,