From 503a7ffd28971e0648ad1a2560d30b3238e39a28 Mon Sep 17 00:00:00 2001 From: Zuhwa Date: Fri, 6 Mar 2026 14:35:55 +0800 Subject: [PATCH] Private memo --- src/acpClient.ts | 69 +++++++++++----------- src/acpJob.ts | 131 ++++++++++++++++++++++++++++++++---------- src/acpJobOffering.ts | 21 +++++-- src/acpMemo.ts | 74 ++++++++++++++++++++---- src/interfaces.ts | 1 + 5 files changed, 219 insertions(+), 77 deletions(-) diff --git a/src/acpClient.ts b/src/acpClient.ts index d2af3cb7..4fc36111 100644 --- a/src/acpClient.ts +++ b/src/acpClient.ts @@ -289,7 +289,7 @@ class AcpClient { callback(true); if (this.onEvaluate) { - const job = this._hydrateJob(data); + const job = await this._hydrateJob(data); this.onEvaluate(job); } @@ -299,7 +299,7 @@ class AcpClient { callback(true); if (this.onNewTask) { - const job = this._hydrateJob(data); + const job = await this._hydrateJob(data); this.onNewTask( job, @@ -353,13 +353,10 @@ class AcpClient { } } - private _hydrateMemo( - memo: IAcpMemoData, - contractClient: BaseAcpContractClient - ): AcpMemo { + private async _hydrateMemo(memo: IAcpMemoData): Promise { try { - return new AcpMemo( - contractClient, + return await AcpMemo.build( + this, memo.id, memo.memoType, memo.content, @@ -378,7 +375,7 @@ class AcpClient { } } - private _hydrateJob(job: IAcpJob): AcpJob { + private async _hydrateJob(job: IAcpJob): Promise { try { return new AcpJob( this, @@ -388,16 +385,10 @@ class AcpClient { job.evaluatorAddress, job.price, job.priceTokenAddress, - job.memos.map((memo) => - this._hydrateMemo( - memo, - this.contractClientByAddress(job.contractAddress) - ) - ), + await Promise.all(job.memos.map((memo) => this._hydrateMemo(memo))), job.phase, job.context, job.contractAddress, - job.deliverable, job.netPayableAmount ); } catch (err) { @@ -405,20 +396,22 @@ class AcpClient { } } - private _hydrateJobs( + private async _hydrateJobs( rawJobs: IAcpJob[], options?: { logPrefix?: string; } - ): AcpJob[] { - const jobs = rawJobs.map((job) => { - try { - return this._hydrateJob(job); - } catch (err) { - console.warn(`${options?.logPrefix ?? "Skipped"}`, err); - return null; - } - }); + ): Promise { + const jobs = await Promise.all( + rawJobs.map((job) => { + try { + return this._hydrateJob(job); + } catch (err) { + console.warn(`${options?.logPrefix ?? "Skipped"}`, err); + return null; + } + }) + ); return jobs.filter((job) => !!job) as AcpJob[]; } @@ -458,7 +451,8 @@ class AcpClient { offering.requiredFunds, offering.slaMinutes, offering.requirement, - offering.deliverable + offering.deliverable, + offering.isPrivate ); }), contractAddress: agent.contractAddress, @@ -617,11 +611,23 @@ class AcpClient { payloads.push(setBudgetWithPaymentTokenPayload); } + const isPrivate = + typeof serviceRequirement === "object" && + "isPrivate" in serviceRequirement && + serviceRequirement.isPrivate; + + let content = preparePayload(serviceRequirement); + + if (isPrivate) { + const memoContent = await this.createMemoContent(jobId, content); + content = memoContent.url; + } + payloads.push( this.acpContractClient.createMemo( jobId, - preparePayload(serviceRequirement), - MemoType.MESSAGE, + content, + isPrivate ? MemoType.OBJECT_URL : MemoType.MESSAGE, true, AcpJobPhases.NEGOTIATION ) @@ -703,10 +709,7 @@ class AcpClient { return null; } - return this._hydrateMemo( - memo, - this.contractClientByAddress(memo.contractAddress) - ); + return this._hydrateMemo(memo); } async getAgent(walletAddress: Address, options: IAcpGetAgentOptions = {}) { diff --git a/src/acpJob.ts b/src/acpJob.ts index e937f43a..70a19508 100644 --- a/src/acpJob.ts +++ b/src/acpJob.ts @@ -24,6 +24,7 @@ class AcpJob { public requirement: Record | string | undefined; public priceType: PriceType = PriceType.FIXED; public priceValue: number = 0; + public isPrivate: boolean = false; constructor( private acpClient: AcpClient, @@ -37,7 +38,6 @@ class AcpJob { public phase: AcpJobPhases, public context: Record, public contractAddress: Address, - private _deliverable: DeliverablePayload | null, public netPayableAmount?: number ) { const content = this.memos.find( @@ -55,6 +55,7 @@ class AcpJob { serviceRequirement: Record; priceType: PriceType; priceValue: number; + isPrivate: boolean; }>(content); if (!contentObj) { @@ -77,6 +78,10 @@ class AcpJob { if (contentObj.priceValue) { this.priceValue = contentObj.priceValue || this.price; } + + if (contentObj.isPrivate) { + this.isPrivate = contentObj.isPrivate; + } } public get acpContractClient() { @@ -126,35 +131,38 @@ class AcpJob { return this.memos[this.memos.length - 1]; } - public async getDeliverable() { - if (!this._deliverable) { - return null; - } - - if (typeof this._deliverable !== "string") { - return this._deliverable; - } - - const regex = /api\/memo-contents\/([0-9]+)$/; - const result = this._deliverable?.match(regex); + public getDeliverable() { + const deliverableMemo = this.memos.find( + (m) => m.nextPhase === AcpJobPhases.COMPLETED + ); - if (!result) { - return this._deliverable; + if (!deliverableMemo) { + return null; } - const deliverable = await this.acpClient.getMemoContent(this._deliverable); - - return tryParseJson(deliverable) || deliverable; + return ( + tryParseJson(deliverableMemo.content) || + deliverableMemo.content + ); } async createRequirement(content: string) { const operations: OperationPayload[] = []; + let finalContent = content; + if (this.isPrivate) { + const memoContent = await this.acpClient.createMemoContent( + this.id, + content + ); + finalContent = memoContent.url; + } + operations.push( this.acpContractClient.createMemo( this.id, - content, - MemoType.MESSAGE, + finalContent, + this.isPrivate ? MemoType.OBJECT_URL : MemoType.MESSAGE, true, AcpJobPhases.TRANSACTION ) @@ -188,6 +196,15 @@ class AcpJob { const isPercentagePricing: boolean = this.priceType === PriceType.PERCENTAGE; + let finalContent = content; + if (this.isPrivate) { + const memoContent = await this.acpClient.createMemoContent( + this.id, + content + ); + finalContent = memoContent.url; + } + if ( amount.fare.chainId && amount.fare.chainId !== this.acpContractClient.config.chain.id @@ -195,7 +212,7 @@ class AcpJob { operations.push( this.acpContractClient.createCrossChainPayableMemo( this.id, - content, + finalContent, amount.fare.contractAddress, amount.amount, recipient, @@ -213,7 +230,7 @@ class AcpJob { operations.push( this.acpContractClient.createPayableMemo( this.id, - content, + finalContent, amount.amount, recipient, isPercentagePricing @@ -290,11 +307,20 @@ class AcpJob { operations.push(this.acpContractClient.signMemo(memo.id, true, reason)); + let finalContent = `Payment made. ${reason ?? ""}`.trim(); + if (this.isPrivate) { + const memoContent = await this.acpClient.createMemoContent( + this.id, + finalContent + ); + finalContent = memoContent.url; + } + operations.push( this.acpContractClient.createMemo( this.id, - `Payment made. ${reason ?? ""}`.trim(), - MemoType.MESSAGE, + finalContent, + this.isPrivate ? MemoType.OBJECT_URL : MemoType.MESSAGE, true, AcpJobPhases.EVALUATION ) @@ -404,12 +430,23 @@ class AcpJob { return await latestMemo.sign(false, memoContent); } + let finalContent = memoContent; + + if (this.isPrivate) { + const memoContent = await this.acpClient.createMemoContent( + this.id, + finalContent + ); + + finalContent = memoContent.url; + } + const operations: OperationPayload[] = []; operations.push( this.acpContractClient.createMemo( this.id, - memoContent, - MemoType.MESSAGE, + finalContent, + this.isPrivate ? MemoType.OBJECT_URL : MemoType.MESSAGE, true, AcpJobPhases.REJECTED ) @@ -434,10 +471,19 @@ class AcpJob { ) ); + let finalContent = memoContent; + if (this.isPrivate) { + const memoContent = await this.acpClient.createMemoContent( + this.id, + finalContent + ); + finalContent = memoContent.url; + } + operations.push( this.acpContractClient.createPayableMemo( this.id, - memoContent, + finalContent, amount.amount, this.clientAddress, feeAmount.amount, @@ -544,10 +590,19 @@ class AcpJob { async createNotification(content: string) { const operations: OperationPayload[] = []; + let finalContent = content; + if (this.isPrivate) { + const memoContent = await this.acpClient.createMemoContent( + this.id, + content + ); + finalContent = memoContent.url; + } + operations.push( this.acpContractClient.createMemo( this.id, - content, + finalContent, MemoType.NOTIFICATION, true, AcpJobPhases.COMPLETED @@ -576,10 +631,19 @@ class AcpJob { const isPercentagePricing: boolean = this.priceType === PriceType.PERCENTAGE && !skipFee; + let finalContent = content; + if (this.isPrivate) { + const memoContent = await this.acpClient.createMemoContent( + this.id, + content + ); + finalContent = memoContent.url; + } + operations.push( this.acpContractClient.createPayableMemo( this.id, - content, + finalContent, amount.amount, this.clientAddress, isPercentagePricing @@ -728,10 +792,19 @@ class AcpJob { const isPercentagePricing: boolean = this.priceType === PriceType.PERCENTAGE && !skipFee; + let finalContent = content; + if (this.isPrivate) { + const memoContent = await this.acpClient.createMemoContent( + this.id, + content + ); + finalContent = memoContent.url; + } + const createMemoOperation = this.acpContractClient.createCrossChainPayableMemo( this.id, - content, + finalContent, amount.fare.contractAddress, amount.amount, recipient, diff --git a/src/acpJobOffering.ts b/src/acpJobOffering.ts index c4c5bc59..b45638ae 100644 --- a/src/acpJobOffering.ts +++ b/src/acpJobOffering.ts @@ -33,7 +33,8 @@ class AcpJobOffering { public requiredFunds: boolean, public slaMinutes: number, public requirement?: Object | string, - public deliverable?: Object | string + public deliverable?: Object | string, + public isPrivate: boolean = true ) { this.ajv = new Ajv({ allErrors: true }); } @@ -60,6 +61,7 @@ class AcpJobOffering { const finalServiceRequirement: Record = { name: this.name, + isPrivate: this.isPrivate, requirement: serviceRequirement, priceValue: this.price, priceType: this.priceType, @@ -116,7 +118,7 @@ class AcpJobOffering { ); } - const { userOpHash } = await this.acpContractClient.handleOperation([ + const { userOpHash } = await this.acpContractClient.handleOperation([ createJobPayload, ]); @@ -139,11 +141,22 @@ class AcpJobOffering { payloads.push(setBudgetWithPaymentTokenPayload); } + let content = JSON.stringify(finalServiceRequirement); + + if (this.isPrivate) { + const memoContent = await this.acpClient.createMemoContent( + jobId, + content + ); + + content = memoContent.url; + } + payloads.push( this.acpContractClient.createMemo( jobId, - JSON.stringify(finalServiceRequirement), - MemoType.MESSAGE, + content, + this.isPrivate ? MemoType.OBJECT_URL : MemoType.MESSAGE, true, AcpJobPhases.NEGOTIATION ) diff --git a/src/acpMemo.ts b/src/acpMemo.ts index bb4d69ee..afc0648c 100644 --- a/src/acpMemo.ts +++ b/src/acpMemo.ts @@ -1,18 +1,15 @@ import { Address } from "viem"; -import BaseAcpContractClient, { +import { AcpJobPhases, MemoType, } from "./contractClients/baseAcpContractClient"; -import { - AcpMemoState, - AcpMemoStatus, - PayableDetails, -} from "./interfaces"; +import { AcpMemoState, AcpMemoStatus, PayableDetails } from "./interfaces"; import util from "util"; +import AcpClient from "./acpClient"; class AcpMemo { constructor( - private contractClient: BaseAcpContractClient, + private acpClient: AcpClient, public id: number, public type: MemoType, public content: string, @@ -32,8 +29,60 @@ class AcpMemo { } } + static async build( + acpClient: AcpClient, + id: number, + type: MemoType, + content: string, + nextPhase: AcpJobPhases, + status: AcpMemoStatus, + senderAddress: Address, + signedReason?: string, + expiry?: Date, + payableDetails?: PayableDetails, + txHash?: `0x${string}`, + signedTxHash?: `0x${string}`, + state?: AcpMemoState + ) { + let memoContent = content; + + const regex = /api\/memo-contents\/([0-9]+)$/; + const result = memoContent.match(regex); + + if (result) { + memoContent = await acpClient.getMemoContent(content); + } + + return new AcpMemo( + acpClient, + id, + type, + memoContent, + nextPhase, + status, + senderAddress, + signedReason, + expiry, + payableDetails, + txHash, + signedTxHash, + state + ); + } + + public async getContent() { + const regex = /api\/memo-contents\/([0-9]+)$/; + const result = this.content.match(regex); + + if (!result) { + return this.content; + } + + return this.acpClient.getMemoContent(this.content); + } + async create(jobId: number, isSecured: boolean = true) { - return this.contractClient.createMemo( + return this.acpClient.acpContractClient.createMemo( jobId, this.content, this.type, @@ -43,8 +92,12 @@ class AcpMemo { } async sign(approved: boolean, reason?: string) { - const payload = this.contractClient.signMemo(this.id, approved, reason); - return await this.contractClient.handleOperation([payload]); + const payload = this.acpClient.acpContractClient.signMemo( + this.id, + approved, + reason + ); + return await this.acpClient.acpContractClient.handleOperation([payload]); } [util.inspect.custom]() { @@ -62,7 +115,6 @@ class AcpMemo { payableDetails: this.payableDetails, }; } - } export default AcpMemo; diff --git a/src/interfaces.ts b/src/interfaces.ts index 9bc69028..d04aacc5 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -135,6 +135,7 @@ export interface IAcpAgent { slaMinutes: number; requirement?: Object | string; deliverable?: Object | string; + isPrivate?: boolean; }[]; resources: { name: string;