diff --git a/package-lock.json b/package-lock.json index 8decc20..8de1543 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@account-kit/smart-contracts": "^4.73.0", "@virtuals-protocol/acp-node": "^0.3.0-beta.10", "ajv": "^8.17.1", + "axios": "^1.13.2", "socket.io-client": "^4.8.1", "tsup": "^8.5.0", "viem": "^2.28.2" @@ -4950,18 +4951,16 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/axios": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", - "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", "license": "MIT", - "optional": true, "dependencies": { "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", + "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, @@ -5383,7 +5382,6 @@ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "license": "MIT", - "optional": true, "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" @@ -5629,7 +5627,6 @@ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "license": "MIT", - "optional": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -5778,7 +5775,6 @@ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "license": "MIT", - "optional": true, "engines": { "node": ">=0.4.0" } @@ -5820,7 +5816,6 @@ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "license": "MIT", - "optional": true, "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", @@ -5960,7 +5955,6 @@ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "license": "MIT", - "optional": true, "engines": { "node": ">= 0.4" } @@ -5970,7 +5964,6 @@ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "license": "MIT", - "optional": true, "engines": { "node": ">= 0.4" } @@ -5980,7 +5973,6 @@ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "license": "MIT", - "optional": true, "dependencies": { "es-errors": "^1.3.0" }, @@ -5993,7 +5985,6 @@ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "license": "MIT", - "optional": true, "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", @@ -6368,7 +6359,6 @@ } ], "license": "MIT", - "optional": true, "engines": { "node": ">=4.0" }, @@ -6395,15 +6385,15 @@ } }, "node_modules/form-data": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", - "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "license": "MIT", - "optional": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { @@ -6435,7 +6425,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "devOptional": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -6466,7 +6455,6 @@ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "license": "MIT", - "optional": true, "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", @@ -6501,7 +6489,6 @@ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "license": "MIT", - "optional": true, "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" @@ -6548,7 +6535,6 @@ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "license": "MIT", - "optional": true, "engines": { "node": ">= 0.4" }, @@ -6610,7 +6596,6 @@ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "license": "MIT", - "optional": true, "engines": { "node": ">= 0.4" }, @@ -6623,7 +6608,6 @@ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "license": "MIT", - "optional": true, "dependencies": { "has-symbols": "^1.0.3" }, @@ -6649,7 +6633,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "devOptional": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -8335,7 +8318,6 @@ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "license": "MIT", - "optional": true, "engines": { "node": ">= 0.4" } @@ -8379,7 +8361,6 @@ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "license": "MIT", - "optional": true, "engines": { "node": ">= 0.6" } @@ -8389,7 +8370,6 @@ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "license": "MIT", - "optional": true, "dependencies": { "mime-db": "1.52.0" }, @@ -8954,8 +8934,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/punycode": { "version": "2.3.1", diff --git a/package.json b/package.json index 4526817..33a7991 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "@account-kit/smart-contracts": "^4.73.0", "@virtuals-protocol/acp-node": "^0.3.0-beta.10", "ajv": "^8.17.1", + "axios": "^1.13.2", "socket.io-client": "^4.8.1", "tsup": "^8.5.0", "viem": "^2.28.2" diff --git a/src/acpClient.ts b/src/acpClient.ts index e86d6f8..8ea802e 100644 --- a/src/acpClient.ts +++ b/src/acpClient.ts @@ -16,8 +16,8 @@ import { IAcpAccount, IAcpClientOptions, IAcpJob, - IAcpJobResponse, - IAcpMemo, + IAcpMemoData, + IAcpResponse, } from "./interfaces"; import AcpError from "./acpError"; import { FareAmountBase } from "./acpFare"; @@ -30,8 +30,9 @@ import { } from "./configs/acpConfigs"; import { preparePayload } from "./utils"; import { USDC_TOKEN_ADDRESS } from "./constants"; +import axios, { AxiosError, AxiosInstance } from "axios"; -const {version} = require("../package.json"); +const { version } = require("../package.json"); enum SocketEvents { ROOM_JOINED = "roomJoined", @@ -62,6 +63,7 @@ class AcpClient { private contractClients: BaseAcpContractClient[]; private onNewTask?: (job: AcpJob, memoToSign?: AcpMemo) => void; private onEvaluate?: (job: AcpJob) => void; + private acpClient: AxiosInstance; constructor(options: IAcpClientOptions) { this.contractClients = Array.isArray(options.acpContractClient) @@ -75,11 +77,18 @@ class AcpClient { this.contractClients.forEach((client) => { if (client.walletAddress !== this.contractClients[0].walletAddress) { throw new AcpError( - "All contract clients must have the same agent wallet address", + "All contract clients must have the same agent wallet address" ); } }); + this.acpClient = axios.create({ + baseURL: `${this.acpUrl}/api`, + headers: { + "wallet-address": this.walletAddress, + }, + }); + this.onNewTask = options.onNewTask; this.onEvaluate = options.onEvaluate || this.defaultOnEvaluate; @@ -92,7 +101,7 @@ class AcpClient { } const result = this.contractClients.find( - (client) => client.contractAddress === address, + (client) => client.contractAddress === address ); if (!result) { @@ -140,94 +149,28 @@ class AcpClient { callback(true); }); - socket.on( - SocketEvents.ON_EVALUATE, - async (data: IAcpJob["data"], callback) => { - callback(true); - - if (this.onEvaluate) { - const job = new AcpJob( - this, - data.id, - data.clientAddress, - data.providerAddress, - data.evaluatorAddress, - data.price, - data.priceTokenAddress, - data.memos.map((memo) => { - return new AcpMemo( - this.contractClientByAddress(data.contractAddress), - memo.id, - memo.memoType, - memo.content, - memo.nextPhase, - memo.status, - memo.senderAddress, - memo.signedReason, - memo.expiry - ? new Date(parseInt(memo.expiry) * 1000) - : undefined, - memo.payableDetails, - memo.txHash, - memo.signedTxHash, - ); - }), - data.phase, - data.context, - data.contractAddress, - data.netPayableAmount, - ); + socket.on(SocketEvents.ON_EVALUATE, async (data: IAcpJob, callback) => { + callback(true); - this.onEvaluate(job); - } - }, - ); + if (this.onEvaluate) { + const job = this._hydrateJob(data); - socket.on( - SocketEvents.ON_NEW_TASK, - async (data: IAcpJob["data"], callback) => { - callback(true); - - if (this.onNewTask) { - const job = new AcpJob( - this, - data.id, - data.clientAddress, - data.providerAddress, - data.evaluatorAddress, - data.price, - data.priceTokenAddress, - data.memos.map((memo) => { - return new AcpMemo( - this.contractClientByAddress(data.contractAddress), - memo.id, - memo.memoType, - memo.content, - memo.nextPhase, - memo.status, - memo.senderAddress, - memo.signedReason, - memo.expiry - ? new Date(parseInt(memo.expiry) * 1000) - : undefined, - memo.payableDetails, - memo.txHash, - memo.signedTxHash, - ); - }), - data.phase, - data.context, - data.contractAddress, - data.netPayableAmount, - ); + this.onEvaluate(job); + } + }); - this.onNewTask( - job, - job.memos.find((m) => m.id == data.memoToSign), - ); - } - }, - ); + socket.on(SocketEvents.ON_NEW_TASK, async (data: IAcpJob, callback) => { + callback(true); + + if (this.onNewTask) { + const job = this._hydrateJob(data); + + this.onNewTask( + job, + job.memos.find((m) => m.id == data.memoToSign) + ); + } + }); const cleanup = async () => { if (socket) { @@ -239,65 +182,166 @@ class AcpClient { process.on("SIGTERM", cleanup); } + private async _fetch( + url: string, + method: "GET" | "POST" | "PUT" | "DELETE" = "GET", + params?: Record, + errCallback?: (err: AxiosError) => void + ): Promise["data"] | undefined> { + try { + const response = await this.acpClient.get>(url, { + params, + }); + + return response.data.data; + } catch (err) { + if (err instanceof AxiosError) { + if (errCallback) { + errCallback(err); + } else if (err.response?.data.error?.message) { + throw new AcpError(err.response?.data.error.message as string); + } + } else { + throw new AcpError("Failed to fetch ACP jobs (network error)", err); + } + } + } + + private _hydrateMemo( + memo: IAcpMemoData, + contractClient: BaseAcpContractClient + ): AcpMemo { + try { + return new AcpMemo( + contractClient, + memo.id, + memo.memoType, + memo.content, + memo.nextPhase, + memo.status, + memo.senderAddress, + memo.signedReason, + memo.expiry ? new Date(parseInt(memo.expiry) * 1000) : undefined, + memo.payableDetails, + memo.txHash, + memo.signedTxHash + ); + } catch (err) { + throw new AcpError(`Failed to hydrate memo ${memo.id}`, err); + } + } + + private _hydrateJob(job: IAcpJob): AcpJob { + try { + return new AcpJob( + this, + job.id, + job.clientAddress, + job.providerAddress, + job.evaluatorAddress, + job.price, + job.priceTokenAddress, + job.memos.map((memo) => + this._hydrateMemo( + memo, + this.contractClientByAddress(job.contractAddress) + ) + ), + job.phase, + job.context, + job.contractAddress, + job.netPayableAmount + ); + } catch (err) { + throw new AcpError(`Failed to hydrate job ${job.id}`, err); + } + } + + private _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; + } + }); + + return jobs.filter((job) => !!job) as AcpJob[]; + } + async browseAgents(keyword: string, options: IAcpBrowseAgentsOptions) { - let { cluster, sort_by, top_k, graduationStatus, onlineStatus, showHiddenOfferings } = options; + let { + cluster, + sort_by, + top_k, + graduationStatus, + onlineStatus, + showHiddenOfferings, + } = options; top_k = top_k ?? 5; - let url = `${this.acpUrl}/api/agents/v4/search?search=${keyword}`; + const params: Record = { + search: keyword, + }; if (sort_by && sort_by.length > 0) { - url += `&sortBy=${sort_by.map((s) => s).join(",")}`; + params.sortBy = sort_by.map((s) => s).join(","); } if (top_k) { - url += `&top_k=${top_k}`; + params.top_k = top_k; } if (this.walletAddress) { - url += `&walletAddressesToExclude=${this.walletAddress}`; + params.walletAddressesToExclude = this.walletAddress; } if (cluster) { - url += `&cluster=${cluster}`; + params.cluster = cluster; } if (graduationStatus) { - url += `&graduationStatus=${graduationStatus}`; + params.graduationStatus = graduationStatus; } if (onlineStatus) { - url += `&onlineStatus=${onlineStatus}`; + params.onlineStatus = onlineStatus; } if (showHiddenOfferings) { - url += `&showHiddenOfferings=${showHiddenOfferings}`; + params.showHiddenOfferings = showHiddenOfferings; } - const response = await fetch(url); + const response = await this.acpClient.get("/agents/v4/search", { params }); const data: { data: AcpAgent[]; - } = await response.json(); + } = response.data; const availableContractClientAddresses = this.contractClients.map( - (client) => client.contractAddress.toLowerCase(), + (client) => client.contractAddress.toLowerCase() ); return data.data .filter( (agent) => - agent.walletAddress.toLowerCase() !== - this.walletAddress.toLowerCase(), + agent.walletAddress.toLowerCase() !== this.walletAddress.toLowerCase() ) .filter((agent) => availableContractClientAddresses.includes( - agent.contractAddress.toLowerCase(), - ), + agent.contractAddress.toLowerCase() + ) ) .map((agent) => { const acpContractClient = this.contractClients.find( (client) => client.contractAddress.toLowerCase() === - agent.contractAddress.toLowerCase(), + agent.contractAddress.toLowerCase() ); if (!acpContractClient) { @@ -316,7 +360,7 @@ class AcpClient { jobs.name, jobs.priceV2.value, jobs.priceV2.type, - jobs.requirement, + jobs.requirement ); }), contractAddress: agent.contractAddress, @@ -333,18 +377,18 @@ class AcpClient { serviceRequirement: Object | string, fareAmount: FareAmountBase, evaluatorAddress?: Address, - expiredAt: Date = new Date(Date.now() + 1000 * 60 * 60 * 24), + expiredAt: Date = new Date(Date.now() + 1000 * 60 * 60 * 24) ) { if (providerAddress === this.walletAddress) { throw new AcpError( - "Provider address cannot be the same as the client address", + "Provider address cannot be the same as the client address" ); } const account = await this.getByClientAndProvider( this.walletAddress, providerAddress, - this.acpContractClient, + this.acpContractClient ); const isV1 = [ @@ -370,22 +414,22 @@ class AcpClient { const createJobPayload = isV1 || !account ? this.acpContractClient.createJob( - providerAddress, - evaluatorAddress || defaultEvaluatorAddress, - expiredAt, - fareAmount.fare.contractAddress, - fareAmount.amount, - "", - isX402Job, - ) + providerAddress, + evaluatorAddress || defaultEvaluatorAddress, + expiredAt, + fareAmount.fare.contractAddress, + fareAmount.amount, + "", + isX402Job + ) : this.acpContractClient.createJobWithAccount( - account.id, - evaluatorAddress || defaultEvaluatorAddress, - fareAmount.amount, - fareAmount.fare.contractAddress, - expiredAt, - isX402Job, - ); + account.id, + evaluatorAddress || defaultEvaluatorAddress, + fareAmount.amount, + fareAmount.fare.contractAddress, + expiredAt, + isX402Job + ); const { userOpHash } = await this.acpContractClient.handleOperation([ createJobPayload, @@ -394,7 +438,7 @@ class AcpClient { const jobId = await this.acpContractClient.getJobId( userOpHash, this.walletAddress, - providerAddress, + providerAddress ); const payloads: OperationPayload[] = []; @@ -403,7 +447,7 @@ class AcpClient { this.acpContractClient.setBudgetWithPaymentToken( jobId, fareAmount.amount, - fareAmount.fare.contractAddress, + fareAmount.fare.contractAddress ); if (setBudgetWithPaymentTokenPayload) { @@ -416,8 +460,8 @@ class AcpClient { preparePayload(serviceRequirement), MemoType.MESSAGE, true, - AcpJobPhases.NEGOTIATION, - ), + AcpJobPhases.NEGOTIATION + ) ); await this.acpContractClient.handleOperation(payloads); @@ -429,264 +473,86 @@ class AcpClient { page: number = 1, pageSize: number = 10 ): Promise { - const url = `${this.acpUrl}/api/jobs/active?pagination[page]=${page}&pagination[pageSize]=${pageSize}`; - const rawJobs = await this._fetchJobList(url); - return this._hydrateJobs(rawJobs, { logPrefix: "Active jobs" }); + const rawJobs = await this._fetch("/jobs/active", "GET", { + pagination: { + page: page, + pageSize: pageSize, + }, + }); + return this._hydrateJobs(rawJobs ?? [], { logPrefix: "Active jobs" }); } async getPendingMemoJobs( page: number = 1, pageSize: number = 10 ): Promise { - const url = `${this.acpUrl}/api/jobs/pending-memos?pagination[page]=${page}&pagination[pageSize]=${pageSize}`; - const rawJobs = await this._fetchJobList(url); - return this._hydrateJobs(rawJobs, { logPrefix: "Pending memo jobs" }); + const rawJobs = await this._fetch("/jobs/pending-memos", "GET", { + pagination: { + page: page, + pageSize: pageSize, + }, + }); + return this._hydrateJobs(rawJobs ?? [], { logPrefix: "Pending memo jobs" }); } async getCompletedJobs( page: number = 1, pageSize: number = 10 ): Promise { - const url = `${this.acpUrl}/api/jobs/completed?pagination[page]=${page}&pagination[pageSize]=${pageSize}`; - const rawJobs = await this._fetchJobList(url); - return this._hydrateJobs(rawJobs, { logPrefix: "Completed jobs" }); + const rawJobs = await this._fetch("/jobs/completed", "GET", { + pagination: { + page: page, + pageSize: pageSize, + }, + }); + return this._hydrateJobs(rawJobs ?? [], { logPrefix: "Completed jobs" }); } async getCancelledJobs( page: number = 1, pageSize: number = 10 ): Promise { - const url = `${this.acpUrl}/api/jobs/cancelled?pagination[page]=${page}&pagination[pageSize]=${pageSize}`; - const rawJobs = await this._fetchJobList(url); - return this._hydrateJobs(rawJobs, { logPrefix: "Cancelled jobs" }); - } - - private async _fetchJobList(url: string): Promise { - let response: Response; - - try { - response = await fetch(url, { - headers: { - "wallet-address": this.walletAddress, - }, - }); - } catch (err) { - throw new AcpError("Failed to fetch ACP jobs (network error)", err); - } - - let data: IAcpJobResponse; - try { - data = await response.json(); - } catch (err) { - throw new AcpError("Failed to parse ACP jobs response", err); - } - - if (data.error) { - throw new AcpError(data.error.message); - } - - return data.data; - } - - private _hydrateJobs( - rawJobs: IAcpJobResponse["data"], - options?: { - logPrefix?: string; - } - ): AcpJob[] { - const jobs: AcpJob[] = []; - const errors: { jobId?: number; error: Error }[] = []; - - for (const job of rawJobs) { - try { - const memos = job.memos.map((memo) => - new AcpMemo( - this.contractClientByAddress(job.contractAddress), - memo.id, - memo.memoType, - memo.content, - memo.nextPhase, - memo.status, - memo.senderAddress, - memo.signedReason, - memo.expiry ? new Date(parseInt(memo.expiry) * 1000) : undefined, - memo.payableDetails, - memo.txHash, - memo.signedTxHash, - ) - ); - - jobs.push( - new AcpJob( - this, - job.id, - job.clientAddress, - job.providerAddress, - job.evaluatorAddress, - job.price, - job.priceTokenAddress, - memos, - job.phase, - job.context, - job.contractAddress, - job.netPayableAmount, - ) - ); - } catch (err) { - errors.push({ jobId: job.id, error: err as Error }); - } - } - - if (errors.length > 0) { - console.warn( - `${options?.logPrefix ?? "Skipped"} ${errors.length} malformed job(s)\n` + - JSON.stringify( - errors.map(e => ({jobId: e.jobId, message: e.error.message})), - null, - 2 - ) - ); - } - - return jobs; + const rawJobs = await this._fetch("/jobs/cancelled", "GET", { + pagination: { + page: page, + pageSize: pageSize, + }, + }); + return this._hydrateJobs(rawJobs ?? [], { logPrefix: "Cancelled jobs" }); } - async getJobById(jobId: number): Promise { - const url = `${this.acpUrl}/api/jobs/${jobId}`; - - let response: Response; - try { - response = await fetch(url, { - headers: { - "wallet-address": this.acpContractClient.walletAddress, - }, - }); - } catch (err) { - throw new AcpError("Failed to fetch job by id (network error)", err); - } - - let data: IAcpJob; - try { - data = await response.json(); - } catch (err) { - throw new AcpError("Failed to parse job by id response", err); - } - - if (data.error) { - throw new AcpError(data.error.message); - } + async getJobById(jobId: number): Promise { + const job = await this._fetch(`/jobs/${jobId}`); - const job = data.data; if (!job) { - return undefined; + return null; } - try { - const memos = job.memos.map( - (memo) => - new AcpMemo( - this.contractClientByAddress(job.contractAddress), - memo.id, - memo.memoType, - memo.content, - memo.nextPhase, - memo.status, - memo.senderAddress, - memo.signedReason, - memo.expiry ? new Date(parseInt(memo.expiry) * 1000) : undefined, - memo.payableDetails, - memo.txHash, - memo.signedTxHash, - ) - ); - - return new AcpJob( - this, - job.id, - job.clientAddress, - job.providerAddress, - job.evaluatorAddress, - job.price, - job.priceTokenAddress, - memos, - job.phase, - job.context, - job.contractAddress, - job.netPayableAmount, - ); - } catch (err) { - throw new AcpError(`Failed to hydrate job ${jobId}`, err); - } + return this._hydrateJob(job); } - async getMemoById( - jobId: number, - memoId: number - ): Promise { - const url = `${this.acpUrl}/api/jobs/${jobId}/memos/${memoId}`; - - let response: Response; - try { - response = await fetch(url, { - headers: { - "wallet-address": this.walletAddress, - }, - }); - } catch (err) { - throw new AcpError("Failed to fetch memo by id (network error)", err); - } - - let data: IAcpMemo; - try { - data = await response.json(); - } catch (err) { - throw new AcpError("Failed to parse memo by id response", err); - } - - if (data.error) { - throw new AcpError(data.error.message); - } + async getMemoById(jobId: number, memoId: number): Promise { + const memo = await this._fetch( + `/jobs/${jobId}/memos/${memoId}` + ); - const memo = data.data; if (!memo) { - return undefined; + return null; } - try { - return new AcpMemo( - this.contractClientByAddress(memo.contractAddress), - memo.id, - memo.memoType, - memo.content, - memo.nextPhase, - memo.status, - memo.senderAddress, - memo.signedReason, - memo.expiry ? new Date(parseInt(memo.expiry) * 1000) : undefined, - memo.payableDetails, - memo.txHash, - memo.signedTxHash, - ); - } catch (err) { - throw new AcpError( - `Failed to hydrate memo ${memoId} for job ${jobId}`, - err - ); - } + return this._hydrateMemo( + memo, + this.contractClientByAddress(memo.contractAddress) + ); } async getAgent(walletAddress: Address) { - const url = `${this.acpUrl}/api/agents?filters[walletAddress]=${walletAddress}`; - - const response = await fetch(url); - const data: { - data: AcpAgent[]; - } = await response.json(); - - const agents = data.data || []; + const agents = await this._fetch( + `/agents?filters[walletAddress]=${walletAddress}` + ); - if (agents.length === 0) { - return; + if (!agents) { + return null; } return agents[0]; @@ -694,59 +560,52 @@ class AcpClient { async getAccountByJobId( jobId: number, - acpContractClient?: BaseAcpContractClient, + 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; - } + const account = await this._fetch(`/accounts/job/${jobId}`); - 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); + if (!account) { + return null; } + + return new AcpAccount( + acpContractClient || this.contractClients[0], + account.id, + account.clientAddress, + account.providerAddress, + account.metadata + ); } async getByClientAndProvider( clientAddress: Address, providerAddress: Address, - acpContractClient?: BaseAcpContractClient, + 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; + const response = await this._fetch( + `/accounts/client/${clientAddress}/provider/${providerAddress}`, + "GET", + {}, + (err) => { + if (err.response?.status === 404) { + console.warn("Account not found by client and provider"); + return; + } + throw new AcpError("Failed to get account by client and provider", err); } + ); - 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); + if (!response) { + return null; } + + return new AcpAccount( + acpContractClient || this.contractClients[0], + response.id, + response.clientAddress, + response.providerAddress, + response.metadata + ); } } diff --git a/src/interfaces.ts b/src/interfaces.ts index 2904914..db86dd6 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -41,10 +41,6 @@ export interface IAcpMemoData { txHash?: `0x${string}`; signedTxHash?: `0x${string}`; } -export interface IAcpMemo { - data: IAcpMemoData; - error?: Error; -} export enum AcpAgentSort { SUCCESSFUL_JOB_COUNT = "successfulJobCount", @@ -66,24 +62,21 @@ export enum AcpOnlineStatus { } export interface IAcpJob { - data: { - id: number; - phase: AcpJobPhases; - description: string; - clientAddress: Address; - providerAddress: Address; - evaluatorAddress: Address; - price: number; - priceTokenAddress: Address; - deliverable: DeliverablePayload | null; - memos: IAcpMemoData[]; - context: Record; - createdAt: string; - contractAddress: Address; - memoToSign?: number; - netPayableAmount?: number; - }; - error?: Error; + id: number; + phase: AcpJobPhases; + description: string; + clientAddress: Address; + providerAddress: Address; + evaluatorAddress: Address; + price: number; + priceTokenAddress: Address; + deliverable: DeliverablePayload | null; + memos: IAcpMemoData[]; + context: Record; + createdAt: string; + contractAddress: Address; + memoToSign?: number; + netPayableAmount?: number; } export interface IAcpJobX402PaymentDetails { @@ -91,17 +84,17 @@ export interface IAcpJobX402PaymentDetails { isBudgetReceived: boolean; } -export interface IAcpJobResponse { - data: IAcpJob["data"][]; +export interface IAcpResponse { + error?: { + message: string; + }; + data: T; meta?: { pagination: { page: number; pageSize: number; - pageCount: number; - total: number; }; }; - error?: Error; } export interface IAcpClientOptions {