@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.
- package/README.md +207 -0
- package/buyer.ts +47 -0
- package/docs/imgs/acp-banner.jpeg +0 -0
- package/examples/acp_base/README.md +94 -0
- package/examples/acp_base/docs/imgs/agent-wallet-page.png +0 -0
- package/examples/acp_base/docs/imgs/session-entity-id-location.png +0 -0
- package/examples/acp_base/docs/imgs/whitelist-wallet-info.png +0 -0
- package/examples/acp_base/docs/imgs/whitelist-wallet.png +0 -0
- package/examples/acp_base/external_evaluation/.env.example +5 -0
- package/examples/acp_base/external_evaluation/buyer.ts +52 -0
- package/examples/acp_base/external_evaluation/env.ts +22 -0
- package/examples/acp_base/external_evaluation/evaluator.ts +29 -0
- package/examples/acp_base/external_evaluation/seller.ts +46 -0
- package/examples/acp_base/self_evaluation/.env.example +4 -0
- package/examples/acp_base/self_evaluation/buyer.ts +54 -0
- package/examples/acp_base/self_evaluation/env.ts +21 -0
- package/examples/acp_base/self_evaluation/seller.ts +46 -0
- package/helpers/.env.example +3 -0
- package/helpers/acpHelperFunctions.ts +69 -0
- package/interfaces.ts +24 -0
- package/package.json +26 -0
- package/seller.ts +46 -0
- package/src/acpAbi.ts +680 -0
- package/src/acpClient.ts +458 -0
- package/src/acpContractClient.ts +269 -0
- package/src/acpJob.ts +91 -0
- package/src/acpJobOffering.ts +43 -0
- package/src/acpMemo.ts +32 -0
- package/src/configs.ts +28 -0
- package/src/index.ts +18 -0
- package/src/interfaces.ts +55 -0
- package/test.ts +26 -0
- package/tsconfig.json +113 -0
package/src/acpClient.ts
ADDED
|
@@ -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;
|