@virtuals-protocol/acp-node 0.1.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,458 @@
1
+ import { Address, parseEther } from "viem";
2
+ import { io } from "socket.io-client";
3
+ import AcpContractClient, { AcpJobPhases, MemoType } from "./acpContractClient";
4
+ import { AcpAgent } from "../interfaces";
5
+ import AcpJob from "./acpJob";
6
+ import AcpMemo from "./acpMemo";
7
+ import AcpJobOffering from "./acpJobOffering";
8
+ import {
9
+ IAcpClientOptions,
10
+ IAcpJob,
11
+ IAcpJobResponse,
12
+ IAcpMemo,
13
+ } from "./interfaces";
14
+
15
+ enum SocketEvents {
16
+ ROOM_JOINED = "roomJoined",
17
+ ON_EVALUATE = "onEvaluate",
18
+ ON_NEW_TASK = "onNewTask",
19
+ }
20
+ export class EvaluateResult {
21
+ isApproved: boolean;
22
+ reasoning: string;
23
+
24
+ constructor(isApproved: boolean, reasoning: string) {
25
+ this.isApproved = isApproved;
26
+ this.reasoning = reasoning;
27
+ }
28
+ }
29
+
30
+ class AcpClient {
31
+ private acpUrl;
32
+ public acpContractClient: AcpContractClient;
33
+ private onNewTask?: (job: AcpJob) => void;
34
+ private onEvaluate?: (job: AcpJob) => void;
35
+
36
+ constructor(options: IAcpClientOptions) {
37
+ this.acpContractClient = options.acpContractClient;
38
+ this.onNewTask = options.onNewTask;
39
+ this.onEvaluate = options.onEvaluate || this.defaultOnEvaluate;
40
+
41
+ this.acpUrl = this.acpContractClient.config.acpUrl;
42
+ this.init();
43
+ }
44
+
45
+ private async defaultOnEvaluate(job: AcpJob) {
46
+ await job.evaluate(true, "Evaluated by default");
47
+ }
48
+
49
+ async init() {
50
+ const socket = io(this.acpUrl, {
51
+ auth: {
52
+ ...(this.onNewTask && {
53
+ walletAddress: this.acpContractClient.walletAddress,
54
+ }),
55
+ ...(this.onEvaluate !== this.defaultOnEvaluate && {
56
+ evaluatorAddress: this.acpContractClient.walletAddress,
57
+ }),
58
+ },
59
+ });
60
+
61
+ socket.on(SocketEvents.ROOM_JOINED, (_, callback) => {
62
+ console.log("Joined ACP Room");
63
+ callback(true);
64
+ });
65
+
66
+ socket.on(
67
+ SocketEvents.ON_EVALUATE,
68
+ async (data: IAcpJob["data"], callback) => {
69
+ callback(true);
70
+
71
+ if (this.onEvaluate) {
72
+ const job = new AcpJob(
73
+ this,
74
+ data.id,
75
+ data.clientAddress,
76
+ data.providerAddress,
77
+ data.evaluatorAddress,
78
+ data.price,
79
+ data.memos.map((memo) => {
80
+ return new AcpMemo(
81
+ this,
82
+ memo.id,
83
+ memo.memoType,
84
+ memo.content,
85
+ memo.nextPhase
86
+ );
87
+ }),
88
+ data.phase
89
+ );
90
+
91
+ this.onEvaluate(job);
92
+ }
93
+ }
94
+ );
95
+
96
+ socket.on(
97
+ SocketEvents.ON_NEW_TASK,
98
+ async (data: IAcpJob["data"], callback) => {
99
+ callback(true);
100
+
101
+ if (this.onNewTask) {
102
+ const job = new AcpJob(
103
+ this,
104
+ data.id,
105
+ data.clientAddress,
106
+ data.providerAddress,
107
+ data.evaluatorAddress,
108
+ data.price,
109
+ data.memos.map((memo) => {
110
+ return new AcpMemo(
111
+ this,
112
+ memo.id,
113
+ memo.memoType,
114
+ memo.content,
115
+ memo.nextPhase
116
+ );
117
+ }),
118
+ data.phase
119
+ );
120
+
121
+ this.onNewTask(job);
122
+ }
123
+ }
124
+ );
125
+
126
+ const cleanup = async () => {
127
+ if (socket) {
128
+ socket.disconnect();
129
+ }
130
+ process.exit(0);
131
+ };
132
+ process.on("SIGINT", cleanup);
133
+ process.on("SIGTERM", cleanup);
134
+ }
135
+
136
+ async browseAgents(keyword: string, cluster?: string) {
137
+ let url = `${this.acpUrl}/api/agents?search=${keyword}&filters[walletAddress][$notIn]=${this.acpContractClient.walletAddress}`;
138
+ if (cluster) {
139
+ url += `&filters[cluster]=${cluster}`;
140
+ }
141
+
142
+ const response = await fetch(url);
143
+ const data: {
144
+ data: AcpAgent[];
145
+ } = await response.json();
146
+
147
+ return data.data.map((agent) => {
148
+ return {
149
+ id: agent.id,
150
+ name: agent.name,
151
+ description: agent.description,
152
+ offerings: agent.offerings.map((offering) => {
153
+ return new AcpJobOffering(
154
+ this,
155
+ agent.walletAddress,
156
+ offering.name,
157
+ offering.price,
158
+ offering.requirementSchema
159
+ );
160
+ }),
161
+ twitterHandle: agent.twitterHandle,
162
+ walletAddress: agent.walletAddress,
163
+ };
164
+ });
165
+ }
166
+
167
+ async initiateJob(
168
+ providerAddress: Address,
169
+ serviceRequirement: Object | string,
170
+ amount: number,
171
+ evaluatorAddress?: Address,
172
+ expiredAt: Date = new Date(Date.now() + 1000 * 60 * 60 * 24)
173
+ ) {
174
+ const { jobId } = await this.acpContractClient.createJob(
175
+ providerAddress,
176
+ evaluatorAddress || this.acpContractClient.walletAddress,
177
+ expiredAt
178
+ );
179
+
180
+ await this.acpContractClient.setBudget(
181
+ jobId,
182
+ parseEther(amount.toString())
183
+ );
184
+
185
+ await this.acpContractClient.createMemo(
186
+ jobId,
187
+ typeof serviceRequirement === "string"
188
+ ? serviceRequirement
189
+ : JSON.stringify(serviceRequirement),
190
+ MemoType.MESSAGE,
191
+ true,
192
+ AcpJobPhases.NEGOTIATION
193
+ );
194
+
195
+ return jobId;
196
+ }
197
+
198
+ async respondJob(
199
+ jobId: number,
200
+ memoId: number,
201
+ accept: boolean,
202
+ reason?: string
203
+ ) {
204
+ await this.acpContractClient.signMemo(memoId, accept, reason);
205
+
206
+ return await this.acpContractClient.createMemo(
207
+ jobId,
208
+ `Job ${jobId} accepted. ${reason ?? ""}`,
209
+ MemoType.MESSAGE,
210
+ false,
211
+ AcpJobPhases.TRANSACTION
212
+ );
213
+ }
214
+
215
+ async payJob(jobId: number, amount: number, memoId: number, reason?: string) {
216
+ await this.acpContractClient.approveAllowance(
217
+ parseEther(amount.toString())
218
+ );
219
+
220
+ await this.acpContractClient.signMemo(memoId, true, reason);
221
+
222
+ return await this.acpContractClient.createMemo(
223
+ jobId,
224
+ `Payment of ${amount} made. ${reason ?? ""}`,
225
+ MemoType.MESSAGE,
226
+ false,
227
+ AcpJobPhases.EVALUATION
228
+ );
229
+ }
230
+
231
+ async deliverJob(jobId: number, deliverable: string) {
232
+ return await this.acpContractClient.createMemo(
233
+ jobId,
234
+ deliverable,
235
+ MemoType.OBJECT_URL,
236
+ true,
237
+ AcpJobPhases.COMPLETED
238
+ );
239
+ }
240
+
241
+ async getActiveJobs(page: number = 1, pageSize: number = 10) {
242
+ let url = `${this.acpUrl}/api/jobs/active?pagination[page]=${page}&pagination[pageSize]=${pageSize}`;
243
+
244
+ try {
245
+ const response = await fetch(url, {
246
+ headers: {
247
+ "wallet-address": this.acpContractClient.walletAddress,
248
+ },
249
+ });
250
+
251
+ const data: IAcpJobResponse = await response.json();
252
+
253
+ if (data.error) {
254
+ throw new Error(data.error.message);
255
+ }
256
+
257
+ return data.data.map((job) => {
258
+ return new AcpJob(
259
+ this,
260
+ job.id,
261
+ job.clientAddress,
262
+ job.providerAddress,
263
+ job.evaluatorAddress,
264
+ job.price,
265
+ job.memos.map((memo) => {
266
+ return new AcpMemo(
267
+ this,
268
+ memo.id,
269
+ memo.memoType,
270
+ memo.content,
271
+ memo.nextPhase
272
+ );
273
+ }),
274
+ job.phase
275
+ );
276
+ });
277
+ } catch (error) {
278
+ throw error;
279
+ }
280
+ }
281
+
282
+ async getCompletedJobs(page: number = 1, pageSize: number = 10) {
283
+ let url = `${this.acpUrl}/api/jobs/completed?pagination[page]=${page}&pagination[pageSize]=${pageSize}`;
284
+
285
+ try {
286
+ const response = await fetch(url, {
287
+ headers: {
288
+ "wallet-address": this.acpContractClient.walletAddress,
289
+ },
290
+ });
291
+
292
+ const data: IAcpJobResponse = await response.json();
293
+
294
+ if (data.error) {
295
+ throw new Error(data.error.message);
296
+ }
297
+
298
+ return data.data.map((job) => {
299
+ return new AcpJob(
300
+ this,
301
+ job.id,
302
+ job.clientAddress,
303
+ job.providerAddress,
304
+ job.evaluatorAddress,
305
+ job.price,
306
+ job.memos.map((memo) => {
307
+ return new AcpMemo(
308
+ this,
309
+ memo.id,
310
+ memo.memoType,
311
+ memo.content,
312
+ memo.nextPhase
313
+ );
314
+ }),
315
+ job.phase
316
+ );
317
+ });
318
+ } catch (error) {
319
+ throw error;
320
+ }
321
+ }
322
+
323
+ async getCancelledJobs(page: number = 1, pageSize: number = 10) {
324
+ let url = `${this.acpUrl}/api/jobs/cancelled?pagination[page]=${page}&pagination[pageSize]=${pageSize}`;
325
+
326
+ try {
327
+ const response = await fetch(url, {
328
+ headers: {
329
+ "wallet-address": this.acpContractClient.walletAddress,
330
+ },
331
+ });
332
+
333
+ const data: IAcpJobResponse = await response.json();
334
+
335
+ if (data.error) {
336
+ throw new Error(data.error.message);
337
+ }
338
+ return data.data.map((job) => {
339
+ return new AcpJob(
340
+ this,
341
+ job.id,
342
+ job.clientAddress,
343
+ job.providerAddress,
344
+ job.evaluatorAddress,
345
+ job.price,
346
+ job.memos.map((memo) => {
347
+ return new AcpMemo(
348
+ this,
349
+ memo.id,
350
+ memo.memoType,
351
+ memo.content,
352
+ memo.nextPhase
353
+ );
354
+ }),
355
+ job.phase
356
+ );
357
+ });
358
+ } catch (error) {
359
+ throw error;
360
+ }
361
+ }
362
+
363
+ async getJobById(jobId: number) {
364
+ let url = `${this.acpUrl}/api/jobs/${jobId}`;
365
+
366
+ try {
367
+ const response = await fetch(url, {
368
+ headers: {
369
+ "wallet-address": this.acpContractClient.walletAddress,
370
+ },
371
+ });
372
+
373
+ const data: IAcpJob = await response.json();
374
+
375
+ if (data.error) {
376
+ throw new Error(data.error.message);
377
+ }
378
+
379
+ const job = data.data;
380
+ if (!job) {
381
+ return;
382
+ }
383
+
384
+ return new AcpJob(
385
+ this,
386
+ job.id,
387
+ job.clientAddress,
388
+ job.providerAddress,
389
+ job.evaluatorAddress,
390
+ job.price,
391
+ job.memos.map((memo) => {
392
+ return new AcpMemo(
393
+ this,
394
+ memo.id,
395
+ memo.memoType,
396
+ memo.content,
397
+ memo.nextPhase
398
+ );
399
+ }),
400
+ job.phase
401
+ );
402
+ } catch (error) {
403
+ throw error;
404
+ }
405
+ }
406
+
407
+ async getMemoById(jobId: number, memoId: number) {
408
+ let url = `${this.acpUrl}/api/jobs/${jobId}/memos/${memoId}`;
409
+
410
+ try {
411
+ const response = await fetch(url, {
412
+ headers: {
413
+ "wallet-address": this.acpContractClient.walletAddress,
414
+ },
415
+ });
416
+
417
+ const data: IAcpMemo = await response.json();
418
+
419
+ if (data.error) {
420
+ throw new Error(data.error.message);
421
+ }
422
+
423
+ const memo = data.data;
424
+ if (!memo) {
425
+ return;
426
+ }
427
+
428
+ return new AcpMemo(
429
+ this,
430
+ memo.id,
431
+ memo.memoType,
432
+ memo.content,
433
+ memo.nextPhase
434
+ );
435
+ } catch (error) {
436
+ throw error;
437
+ }
438
+ }
439
+
440
+ async getAgent(walletAddress: Address) {
441
+ const url = `${this.acpUrl}/api/agents?filters[walletAddress]=${walletAddress}`;
442
+
443
+ const response = await fetch(url);
444
+ const data: {
445
+ data: AcpAgent[];
446
+ } = await response.json();
447
+
448
+ const agents = data.data || [];
449
+
450
+ if (agents.length === 0) {
451
+ return;
452
+ }
453
+
454
+ return agents[0];
455
+ }
456
+ }
457
+
458
+ export default AcpClient;
@@ -0,0 +1,269 @@
1
+ import { Address, LocalAccountSigner, SmartAccountSigner } from "@aa-sdk/core";
2
+ import { alchemy } from "@account-kit/infra";
3
+ import {
4
+ ModularAccountV2Client,
5
+ createModularAccountV2Client,
6
+ } from "@account-kit/smart-contracts";
7
+ import { AcpContractConfig } from "./configs";
8
+ import ACP_ABI from "./acpAbi";
9
+ import { encodeFunctionData, erc20Abi, fromHex } from "viem";
10
+
11
+ export enum MemoType {
12
+ MESSAGE,
13
+ CONTEXT_URL,
14
+ IMAGE_URL,
15
+ VOICE_URL,
16
+ OBJECT_URL,
17
+ TXHASH,
18
+ }
19
+
20
+ export enum AcpJobPhases {
21
+ REQUEST = 0,
22
+ NEGOTIATION = 1,
23
+ TRANSACTION = 2,
24
+ EVALUATION = 3,
25
+ COMPLETED = 4,
26
+ REJECTED = 5,
27
+ }
28
+
29
+ class AcpContractClient {
30
+ private _sessionKeyClient: ModularAccountV2Client | undefined;
31
+
32
+ private chain;
33
+ private contractAddress: Address;
34
+ private virtualsTokenAddress: Address;
35
+
36
+ constructor(
37
+ private walletPrivateKey: Address,
38
+ private sessionEntityKeyId: number,
39
+ private agentWalletAddress: Address,
40
+ public config: AcpContractConfig
41
+ ) {
42
+ this.chain = config.chain;
43
+ this.contractAddress = config.contractAddress;
44
+ this.virtualsTokenAddress = config.virtualsTokenAddress;
45
+ }
46
+
47
+ static async build(
48
+ walletPrivateKey: Address,
49
+ sessionEntityKeyId: number,
50
+ agentWalletAddress: Address,
51
+ config: AcpContractConfig
52
+ ) {
53
+ const acpContractClient = new AcpContractClient(
54
+ walletPrivateKey,
55
+ sessionEntityKeyId,
56
+ agentWalletAddress,
57
+ config
58
+ );
59
+
60
+ await acpContractClient.init();
61
+
62
+ return acpContractClient;
63
+ }
64
+
65
+ async init() {
66
+ const sessionKeySigner: SmartAccountSigner =
67
+ LocalAccountSigner.privateKeyToAccountSigner(this.walletPrivateKey);
68
+
69
+ this._sessionKeyClient = await createModularAccountV2Client({
70
+ chain: this.chain,
71
+ transport: alchemy({
72
+ rpcUrl: this.config.alchemyRpcUrl,
73
+ }),
74
+ signer: sessionKeySigner,
75
+ policyId: "186aaa4a-5f57-4156-83fb-e456365a8820",
76
+ accountAddress: this.agentWalletAddress,
77
+ signerEntity: {
78
+ entityId: this.sessionEntityKeyId,
79
+ isGlobalValidation: true,
80
+ },
81
+ });
82
+ }
83
+
84
+ get sessionKeyClient() {
85
+ if (!this._sessionKeyClient) {
86
+ throw new Error("Session key client not initialized");
87
+ }
88
+
89
+ return this._sessionKeyClient;
90
+ }
91
+
92
+ get walletAddress() {
93
+ return this.sessionKeyClient.account.address as Address;
94
+ }
95
+
96
+ private async getJobId(hash: Address) {
97
+ const result = await this.sessionKeyClient.getUserOperationReceipt(hash);
98
+
99
+ if (!result) {
100
+ throw new Error("Failed to get user operation receipt");
101
+ }
102
+
103
+ const contractLogs = result.logs.find(
104
+ (log: any) =>
105
+ log.address.toLowerCase() === this.contractAddress.toLowerCase()
106
+ ) as any;
107
+
108
+ if (!contractLogs) {
109
+ throw new Error("Failed to get contract logs");
110
+ }
111
+
112
+ return fromHex(contractLogs.data, "number");
113
+ }
114
+
115
+ async createJob(
116
+ providerAddress: string,
117
+ evaluatorAddress: string,
118
+ expireAt: Date
119
+ ): Promise<{ txHash: string; jobId: number }> {
120
+ try {
121
+ const data = encodeFunctionData({
122
+ abi: ACP_ABI,
123
+ functionName: "createJob",
124
+ args: [
125
+ providerAddress,
126
+ evaluatorAddress,
127
+ Math.floor(expireAt.getTime() / 1000),
128
+ ],
129
+ });
130
+
131
+ const { hash } = await this.sessionKeyClient.sendUserOperation({
132
+ uo: {
133
+ target: this.contractAddress,
134
+ data: data,
135
+ },
136
+ });
137
+
138
+ await this.sessionKeyClient.waitForUserOperationTransaction({
139
+ hash,
140
+ });
141
+
142
+ const jobId = await this.getJobId(hash);
143
+
144
+ return { txHash: hash, jobId: jobId };
145
+ } catch (error) {
146
+ console.error(error);
147
+ throw new Error("Failed to create job");
148
+ }
149
+ }
150
+
151
+ async approveAllowance(priceInWei: bigint) {
152
+ const data = encodeFunctionData({
153
+ abi: erc20Abi,
154
+ functionName: "approve",
155
+ args: [this.contractAddress, priceInWei],
156
+ });
157
+
158
+ const { hash } = await this.sessionKeyClient.sendUserOperation({
159
+ uo: {
160
+ target: this.virtualsTokenAddress,
161
+ data: data,
162
+ },
163
+ });
164
+
165
+ await this.sessionKeyClient.waitForUserOperationTransaction({
166
+ hash,
167
+ });
168
+
169
+ return hash;
170
+ }
171
+
172
+ async createMemo(
173
+ jobId: number,
174
+ content: string,
175
+ type: MemoType,
176
+ isSecured: boolean,
177
+ nextPhase: AcpJobPhases
178
+ ): Promise<Address> {
179
+ let retries = 3;
180
+ while (retries > 0) {
181
+ try {
182
+ const data = encodeFunctionData({
183
+ abi: ACP_ABI,
184
+ functionName: "createMemo",
185
+ args: [jobId, content, type, isSecured, nextPhase],
186
+ });
187
+
188
+ const { hash } = await this.sessionKeyClient.sendUserOperation({
189
+ uo: {
190
+ target: this.contractAddress,
191
+ data: data,
192
+ },
193
+ });
194
+
195
+ await this.sessionKeyClient.waitForUserOperationTransaction({
196
+ hash,
197
+ });
198
+
199
+ return hash;
200
+ } catch (error) {
201
+ console.error(`failed to create memo ${jobId} ${content} ${error}`);
202
+ retries -= 1;
203
+ await new Promise((resolve) => setTimeout(resolve, 2000 * retries));
204
+ }
205
+ }
206
+
207
+ throw new Error("Failed to create memo");
208
+ }
209
+
210
+ async signMemo(memoId: number, isApproved: boolean, reason?: string) {
211
+ let retries = 3;
212
+ while (retries > 0) {
213
+ try {
214
+ const data = encodeFunctionData({
215
+ abi: ACP_ABI,
216
+ functionName: "signMemo",
217
+ args: [memoId, isApproved, reason],
218
+ });
219
+
220
+ const { hash } = await this.sessionKeyClient.sendUserOperation({
221
+ uo: {
222
+ target: this.contractAddress,
223
+ data: data,
224
+ },
225
+ });
226
+
227
+ await this.sessionKeyClient.waitForUserOperationTransaction({
228
+ hash,
229
+ });
230
+
231
+ return hash;
232
+ } catch (error) {
233
+ console.error(`failed to sign memo ${error}`);
234
+ retries -= 1;
235
+ await new Promise((resolve) => setTimeout(resolve, 2000 * retries));
236
+ }
237
+ }
238
+
239
+ throw new Error("Failed to sign memo");
240
+ }
241
+
242
+ async setBudget(jobId: number, budget: bigint) {
243
+ try {
244
+ const data = encodeFunctionData({
245
+ abi: ACP_ABI,
246
+ functionName: "setBudget",
247
+ args: [jobId, budget],
248
+ });
249
+
250
+ const { hash } = await this.sessionKeyClient.sendUserOperation({
251
+ uo: {
252
+ target: this.contractAddress,
253
+ data: data,
254
+ },
255
+ });
256
+
257
+ await this.sessionKeyClient.waitForUserOperationTransaction({
258
+ hash,
259
+ });
260
+
261
+ return hash;
262
+ } catch (error) {
263
+ console.error(error);
264
+ throw new Error("Failed to set budget");
265
+ }
266
+ }
267
+ }
268
+
269
+ export default AcpContractClient;