@bountyagents/bountyagents-task 2026.2.26
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/.env.local +1 -0
- package/dist/escrow.d.ts +3 -0
- package/dist/escrow.js +14 -0
- package/dist/escrow.js.map +1 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.js +42394 -0
- package/dist/index.js.map +1 -0
- package/dist/mizu.d.ts +27 -0
- package/dist/mizu.js +53 -0
- package/dist/mizu.js.map +1 -0
- package/dist/mizuPublisher.d.ts +27 -0
- package/dist/mizuPublisher.js +53 -0
- package/dist/mizuPublisher.js.map +1 -0
- package/dist/signers.d.ts +13 -0
- package/dist/signers.js +26 -0
- package/dist/signers.js.map +1 -0
- package/dist/signing.d.ts +21 -0
- package/dist/signing.js +72 -0
- package/dist/signing.js.map +1 -0
- package/dist/taskPublisher.d.ts +27 -0
- package/dist/taskPublisher.js +53 -0
- package/dist/taskPublisher.js.map +1 -0
- package/dist/token.d.ts +5 -0
- package/dist/token.js +12 -0
- package/dist/token.js.map +1 -0
- package/dist/types.d.ts +187 -0
- package/dist/types.js +77 -0
- package/dist/types.js.map +1 -0
- package/index.ts +34 -0
- package/openclaw.plugin.json +10 -0
- package/package.json +38 -0
- package/src/escrow.ts +27 -0
- package/src/index.ts +413 -0
- package/src/signers.ts +36 -0
- package/src/signing.ts +113 -0
- package/src/token.ts +17 -0
- package/src/types.ts +101 -0
- package/tsconfig.json +16 -0
package/dist/types.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { responseStatusSchema, taskStatusSchema } from '@bountyagents/task-db';
|
|
3
|
+
import { getAddress, isAddress } from 'viem';
|
|
4
|
+
const addressSchema = z
|
|
5
|
+
.string()
|
|
6
|
+
.refine((value) => isAddress(value), {
|
|
7
|
+
message: 'Invalid address'
|
|
8
|
+
})
|
|
9
|
+
.transform((value) => getAddress(value));
|
|
10
|
+
const tokenSchema = z.string().regex(/^[a-z0-9-]+:0x[a-fA-F0-9]{40}$/);
|
|
11
|
+
const priceStringSchema = z.string().regex(/^[0-9]+$/).refine((val) => BigInt(val) > 0n, {
|
|
12
|
+
message: 'price must be greater than zero'
|
|
13
|
+
});
|
|
14
|
+
const signatureSchema = z.string().regex(/^0x[a-fA-F0-9]{130}$/, { message: 'Invalid signature' });
|
|
15
|
+
export const createTaskPayloadSchema = z.object({
|
|
16
|
+
id: z.string().uuid(),
|
|
17
|
+
title: z.string().min(4),
|
|
18
|
+
content: z.string().min(1)
|
|
19
|
+
});
|
|
20
|
+
export const fundTaskPayloadSchema = z.object({
|
|
21
|
+
taskId: z.string().uuid(),
|
|
22
|
+
price: priceStringSchema,
|
|
23
|
+
token: tokenSchema
|
|
24
|
+
});
|
|
25
|
+
export const submitResponsePayloadSchema = z.object({
|
|
26
|
+
id: z.string().uuid().optional(),
|
|
27
|
+
taskId: z.string().uuid(),
|
|
28
|
+
payload: z.string().min(1)
|
|
29
|
+
});
|
|
30
|
+
export const decisionPayloadSchema = z
|
|
31
|
+
.object({
|
|
32
|
+
responseId: z.string().uuid(),
|
|
33
|
+
workerAddress: addressSchema,
|
|
34
|
+
price: z.string(),
|
|
35
|
+
status: responseStatusSchema,
|
|
36
|
+
settlementSignature: signatureSchema.optional()
|
|
37
|
+
})
|
|
38
|
+
.refine((value) => value.status !== 'pending', {
|
|
39
|
+
message: 'Status must be approved or rejected'
|
|
40
|
+
})
|
|
41
|
+
.refine((value) => (value.status === 'approved' ? Boolean(value.settlementSignature) : true), {
|
|
42
|
+
message: 'settlementSignature required when approving'
|
|
43
|
+
});
|
|
44
|
+
export const cancelTaskPayloadSchema = z.object({
|
|
45
|
+
taskId: z.string().uuid()
|
|
46
|
+
});
|
|
47
|
+
export const settleTaskPayloadSchema = z.object({
|
|
48
|
+
taskId: z.string().uuid(),
|
|
49
|
+
responseId: z.string().uuid()
|
|
50
|
+
});
|
|
51
|
+
const createdRangeSchema = z.tuple([z.number().nonnegative(), z.number().nonnegative()]).optional();
|
|
52
|
+
export const taskQueryPayloadSchema = z.object({
|
|
53
|
+
filter: z
|
|
54
|
+
.object({
|
|
55
|
+
publisher: addressSchema.optional(),
|
|
56
|
+
created_at: createdRangeSchema,
|
|
57
|
+
status: taskStatusSchema.optional(),
|
|
58
|
+
minPrice: z.number().int().nonnegative().optional(),
|
|
59
|
+
keyword: z.string().min(2).max(256).optional()
|
|
60
|
+
})
|
|
61
|
+
.default({}),
|
|
62
|
+
sortBy: z.enum(['price', 'created_at']).optional().default('created_at'),
|
|
63
|
+
pageSize: z.number().int().min(1).max(200).optional().default(50),
|
|
64
|
+
pageNum: z.number().int().min(0).optional().default(0)
|
|
65
|
+
});
|
|
66
|
+
export const taskResponsesQueryPayloadSchema = z.object({
|
|
67
|
+
taskId: z.string().uuid(),
|
|
68
|
+
workerAddress: addressSchema.optional(),
|
|
69
|
+
pageSize: z.number().int().min(1).max(200).optional().default(50),
|
|
70
|
+
pageNum: z.number().int().min(0).optional().default(0)
|
|
71
|
+
});
|
|
72
|
+
export const workerResponsesQueryPayloadSchema = z.object({
|
|
73
|
+
taskId: z.string().uuid().optional(),
|
|
74
|
+
pageSize: z.number().int().min(1).max(200).optional().default(50),
|
|
75
|
+
pageNum: z.number().int().min(0).optional().default(0)
|
|
76
|
+
});
|
|
77
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/index.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { BountyAgentsPublisherPlugin } from "./src/index.js";
|
|
2
|
+
import { PrivateKeySigner } from "./src/signers.js";
|
|
3
|
+
import crypto from "node:crypto";
|
|
4
|
+
|
|
5
|
+
export default function (api: any) {
|
|
6
|
+
api.registerCli(
|
|
7
|
+
({ program }: { program: any }) => {
|
|
8
|
+
program.command("createtask").action(async () => {
|
|
9
|
+
const signer = new PrivateKeySigner(
|
|
10
|
+
"0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
|
|
11
|
+
);
|
|
12
|
+
const plugin = new BountyAgentsPublisherPlugin(signer, {
|
|
13
|
+
serviceUrl: "http://localhost:3000",
|
|
14
|
+
contractAddress: "0x55D45aFA265d0381C8A81328FfeA408D2Dd45F40",
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const task = await plugin.executeTool(
|
|
19
|
+
"bountyagents.publisher.task.create",
|
|
20
|
+
{
|
|
21
|
+
id: crypto.randomUUID(),
|
|
22
|
+
title: "Test Task from CLI",
|
|
23
|
+
content: "This is a test task created via the CLI tool.",
|
|
24
|
+
}
|
|
25
|
+
);
|
|
26
|
+
console.log("Task created successfully:", task);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
console.error("Failed to create task:", error);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
},
|
|
32
|
+
{ commands: ["createtask"] }
|
|
33
|
+
);
|
|
34
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bountyagents/bountyagents-task",
|
|
3
|
+
"version": "2026.2.26",
|
|
4
|
+
"description": "BountyAgents Task Plugin for OpenClaw",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "bun build ./index.ts --outdir ./dist --target node --format esm --external openclaw/plugin-sdk",
|
|
9
|
+
"watch": "bun build ./index.ts --outdir ./dist --target node --format esm --external openclaw/plugin-sdk --watch",
|
|
10
|
+
"prepublishOnly": "npm run build"
|
|
11
|
+
},
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"keywords": [
|
|
14
|
+
"openclaw",
|
|
15
|
+
"openclaw-plugin",
|
|
16
|
+
"task",
|
|
17
|
+
"bountyagents"
|
|
18
|
+
],
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@sinclair/typebox": "^0.34.48",
|
|
21
|
+
"zod": "^4.3.6"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"openclaw": "*"
|
|
25
|
+
},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"openclaw": "*"
|
|
28
|
+
},
|
|
29
|
+
"openclaw": {
|
|
30
|
+
"extensions": [
|
|
31
|
+
"./dist/index.js"
|
|
32
|
+
],
|
|
33
|
+
"install": {
|
|
34
|
+
"npmSpec": "@bountyagents/bountyagents-task",
|
|
35
|
+
"defaultChoice": "npm"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
package/src/escrow.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Hex, encodePacked, getAddress, keccak256, stringToBytes } from 'viem';
|
|
2
|
+
|
|
3
|
+
export const taskDepositKey = (taskId: string): Hex => keccak256(stringToBytes(taskId));
|
|
4
|
+
|
|
5
|
+
export const buildSettleDataHash = (
|
|
6
|
+
contractAddress: string,
|
|
7
|
+
key: Hex,
|
|
8
|
+
owner: string,
|
|
9
|
+
token: string,
|
|
10
|
+
worker: string,
|
|
11
|
+
amount: bigint
|
|
12
|
+
): Hex => {
|
|
13
|
+
return keccak256(
|
|
14
|
+
encodePacked(
|
|
15
|
+
['string', 'address', 'bytes32', 'address', 'address', 'address', 'uint256'],
|
|
16
|
+
[
|
|
17
|
+
'SETTLE',
|
|
18
|
+
getAddress(contractAddress as `0x${string}`),
|
|
19
|
+
key,
|
|
20
|
+
getAddress(owner as `0x${string}`),
|
|
21
|
+
getAddress(token as `0x${string}`),
|
|
22
|
+
getAddress(worker as `0x${string}`),
|
|
23
|
+
amount
|
|
24
|
+
]
|
|
25
|
+
)
|
|
26
|
+
);
|
|
27
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
import { fetch } from "undici";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import {
|
|
4
|
+
TaskRecord,
|
|
5
|
+
ResponseRecord,
|
|
6
|
+
taskRecordSchema,
|
|
7
|
+
responseRecordSchema,
|
|
8
|
+
} from "@bountyagents/task-db";
|
|
9
|
+
import { getAddress, Hex } from "viem";
|
|
10
|
+
import { Signer } from "./signers.js";
|
|
11
|
+
import {
|
|
12
|
+
CancelTaskPayload,
|
|
13
|
+
CreateTaskPayload,
|
|
14
|
+
DecisionPayload,
|
|
15
|
+
FundTaskPayload,
|
|
16
|
+
SettleTaskPayload,
|
|
17
|
+
SubmitResponsePayload,
|
|
18
|
+
TaskQueryPayload,
|
|
19
|
+
TaskResponsesQueryPayload,
|
|
20
|
+
WorkerResponsesQueryPayload,
|
|
21
|
+
cancelTaskPayloadSchema,
|
|
22
|
+
createTaskPayloadSchema,
|
|
23
|
+
decisionPayloadSchema,
|
|
24
|
+
fundTaskPayloadSchema,
|
|
25
|
+
settleTaskPayloadSchema,
|
|
26
|
+
submitResponsePayloadSchema,
|
|
27
|
+
taskQueryPayloadSchema,
|
|
28
|
+
taskResponsesQueryPayloadSchema,
|
|
29
|
+
workerResponsesQueryPayloadSchema,
|
|
30
|
+
} from "./types.js";
|
|
31
|
+
import {
|
|
32
|
+
cancelTaskSignaturePayload,
|
|
33
|
+
decisionSignaturePayload,
|
|
34
|
+
responseSignaturePayload,
|
|
35
|
+
taskFundSignaturePayload,
|
|
36
|
+
taskResponsesQuerySignaturePayload,
|
|
37
|
+
taskSettleSignaturePayload,
|
|
38
|
+
taskSignaturePayload,
|
|
39
|
+
workerResponsesQuerySignaturePayload,
|
|
40
|
+
} from "./signing.js";
|
|
41
|
+
import { parseTokenIdentifier } from "./token.js";
|
|
42
|
+
import { buildSettleDataHash, taskDepositKey } from "./escrow.js";
|
|
43
|
+
|
|
44
|
+
export interface BountyAgentsPluginOptions {
|
|
45
|
+
serviceUrl: string;
|
|
46
|
+
contractAddress: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface PluginTool {
|
|
50
|
+
name: string;
|
|
51
|
+
description: string;
|
|
52
|
+
inputSchema: z.ZodTypeAny;
|
|
53
|
+
execute: (input: unknown) => Promise<unknown>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const normalizeUrl = (url: string) => url.replace(/\/$/, "");
|
|
57
|
+
|
|
58
|
+
const taskResponseSchema = z.object({ task: taskRecordSchema });
|
|
59
|
+
const submissionResponseSchema = z.object({ response: responseRecordSchema });
|
|
60
|
+
const tasksListSchema = z.object({ tasks: z.array(taskRecordSchema) });
|
|
61
|
+
const responsesListSchema = z.object({
|
|
62
|
+
responses: z.array(responseRecordSchema),
|
|
63
|
+
});
|
|
64
|
+
const settleResponseSchema = z.object({
|
|
65
|
+
task: taskRecordSchema,
|
|
66
|
+
settlementSignature: z.string(),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
abstract class BaseBountyPlugin {
|
|
70
|
+
protected readonly baseUrl: string;
|
|
71
|
+
protected readonly contractAddress: `0x${string}`;
|
|
72
|
+
private readonly tools: PluginTool[] = [];
|
|
73
|
+
|
|
74
|
+
constructor(
|
|
75
|
+
protected readonly signer: Signer,
|
|
76
|
+
options: BountyAgentsPluginOptions
|
|
77
|
+
) {
|
|
78
|
+
this.baseUrl = normalizeUrl(options.serviceUrl);
|
|
79
|
+
this.contractAddress = getAddress(options.contractAddress as `0x${string}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
getTools(): PluginTool[] {
|
|
83
|
+
return this.tools;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async executeTool(name: string, input: unknown): Promise<unknown> {
|
|
87
|
+
const tool = this.tools.find((t) => t.name === name);
|
|
88
|
+
if (!tool) {
|
|
89
|
+
throw new Error(`Tool ${name} not found`);
|
|
90
|
+
}
|
|
91
|
+
return tool.execute(input);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
protected registerTool<TSchema extends z.ZodTypeAny>(
|
|
95
|
+
name: string,
|
|
96
|
+
description: string,
|
|
97
|
+
schema: TSchema,
|
|
98
|
+
executor: (input: z.infer<TSchema>) => Promise<unknown>
|
|
99
|
+
) {
|
|
100
|
+
this.tools.push({
|
|
101
|
+
name,
|
|
102
|
+
description,
|
|
103
|
+
inputSchema: schema,
|
|
104
|
+
execute: (rawInput: unknown) => executor(schema.parse(rawInput)),
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
protected async request(
|
|
109
|
+
path: string,
|
|
110
|
+
body?: unknown,
|
|
111
|
+
method: "GET" | "POST" = "POST"
|
|
112
|
+
): Promise<unknown> {
|
|
113
|
+
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
114
|
+
method,
|
|
115
|
+
headers: {
|
|
116
|
+
"content-type": "application/json",
|
|
117
|
+
},
|
|
118
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
119
|
+
});
|
|
120
|
+
const payload = await response.text();
|
|
121
|
+
const data = payload ? JSON.parse(payload) : {};
|
|
122
|
+
if (!response.ok) {
|
|
123
|
+
const errorMessage =
|
|
124
|
+
typeof data.message === "string" ? data.message : response.statusText;
|
|
125
|
+
throw new Error(`Service error: ${errorMessage}`);
|
|
126
|
+
}
|
|
127
|
+
return data;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
protected signPayload(payload: string): Promise<Hex> {
|
|
131
|
+
return this.signer.signMessage(payload);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
protected signDigest(digest: Hex): Promise<Hex> {
|
|
135
|
+
return this.signer.signDigest(digest);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
protected async fetchTask(taskId: string): Promise<TaskRecord> {
|
|
139
|
+
const response = await this.request(`/tasks/${taskId}`, undefined, "GET");
|
|
140
|
+
return taskResponseSchema.parse(response).task;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
protected async fetchResponse(responseId: string): Promise<ResponseRecord> {
|
|
144
|
+
const response = await this.request(
|
|
145
|
+
`/responses/${responseId}`,
|
|
146
|
+
undefined,
|
|
147
|
+
"GET"
|
|
148
|
+
);
|
|
149
|
+
return submissionResponseSchema.parse(response).response;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export class BountyAgentsPublisherPlugin extends BaseBountyPlugin {
|
|
154
|
+
constructor(signer: Signer, options: BountyAgentsPluginOptions) {
|
|
155
|
+
super(signer, options);
|
|
156
|
+
this.registerTool(
|
|
157
|
+
"bountyagents.publisher.task.create",
|
|
158
|
+
"Create a draft bounty task on the remote service",
|
|
159
|
+
createTaskPayloadSchema,
|
|
160
|
+
(input) => this.createTask(input)
|
|
161
|
+
);
|
|
162
|
+
this.registerTool(
|
|
163
|
+
"bountyagents.publisher.task.fund",
|
|
164
|
+
"Attach price/token metadata after depositing escrow funds",
|
|
165
|
+
fundTaskPayloadSchema,
|
|
166
|
+
(input) => this.fundTask(input)
|
|
167
|
+
);
|
|
168
|
+
this.registerTool(
|
|
169
|
+
"bountyagents.publisher.task.decision",
|
|
170
|
+
"Approve or reject a response and cache the settlement signature",
|
|
171
|
+
decisionPayloadSchema,
|
|
172
|
+
(input) => this.decideOnResponse(input)
|
|
173
|
+
);
|
|
174
|
+
this.registerTool(
|
|
175
|
+
"bountyagents.publisher.task.cancel",
|
|
176
|
+
"Cancel an active task and retrieve the admin withdraw signature",
|
|
177
|
+
cancelTaskPayloadSchema,
|
|
178
|
+
(input) => this.cancelTask(input)
|
|
179
|
+
);
|
|
180
|
+
this.registerTool(
|
|
181
|
+
"bountyagents.publisher.task.query",
|
|
182
|
+
"Search tasks with keyword, publisher and price filters",
|
|
183
|
+
taskQueryPayloadSchema,
|
|
184
|
+
(input) => this.queryTasks(input)
|
|
185
|
+
);
|
|
186
|
+
this.registerTool(
|
|
187
|
+
"bountyagents.publisher.task.response.query",
|
|
188
|
+
"List responses for a task (owner signature required)",
|
|
189
|
+
taskResponsesQueryPayloadSchema,
|
|
190
|
+
(input) => this.queryTaskResponses(input)
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private async createTask(payload: CreateTaskPayload): Promise<TaskRecord> {
|
|
195
|
+
const body = {
|
|
196
|
+
...payload,
|
|
197
|
+
ownerAddress: this.signer.address,
|
|
198
|
+
signature: await this.signPayload(taskSignaturePayload(payload)),
|
|
199
|
+
};
|
|
200
|
+
const response = await this.request("/tasks", body);
|
|
201
|
+
return taskResponseSchema.parse(response).task;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
private async fundTask(payload: FundTaskPayload): Promise<TaskRecord> {
|
|
205
|
+
const body = {
|
|
206
|
+
...payload,
|
|
207
|
+
ownerAddress: this.signer.address,
|
|
208
|
+
signature: await this.signPayload(taskFundSignaturePayload(payload)),
|
|
209
|
+
};
|
|
210
|
+
const response = await this.request(`/tasks/${payload.taskId}/fund`, body);
|
|
211
|
+
return taskResponseSchema.parse(response).task;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
private async decideOnResponse(
|
|
215
|
+
payload: DecisionPayload
|
|
216
|
+
): Promise<ResponseRecord> {
|
|
217
|
+
const responseRecord = await this.fetchResponse(payload.responseId);
|
|
218
|
+
const task = await this.fetchTask(responseRecord.task_id);
|
|
219
|
+
if (!task.token || task.price === "0") {
|
|
220
|
+
throw new Error("Task is not funded yet");
|
|
221
|
+
}
|
|
222
|
+
if (payload.price !== task.price) {
|
|
223
|
+
throw new Error("Price does not match funded amount");
|
|
224
|
+
}
|
|
225
|
+
const workerAddress = getAddress(payload.workerAddress as `0x${string}`);
|
|
226
|
+
if (workerAddress !== getAddress(responseRecord.worker as `0x${string}`)) {
|
|
227
|
+
throw new Error("Worker address mismatch");
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const settlementSignature =
|
|
231
|
+
payload.status === "approved"
|
|
232
|
+
? await this.createSettlementSignature(
|
|
233
|
+
responseRecord.task_id,
|
|
234
|
+
workerAddress,
|
|
235
|
+
payload.price,
|
|
236
|
+
task.token
|
|
237
|
+
)
|
|
238
|
+
: undefined;
|
|
239
|
+
|
|
240
|
+
const canonicalPayload: DecisionPayload = {
|
|
241
|
+
responseId: payload.responseId,
|
|
242
|
+
workerAddress,
|
|
243
|
+
price: payload.price,
|
|
244
|
+
status: payload.status,
|
|
245
|
+
settlementSignature,
|
|
246
|
+
};
|
|
247
|
+
const body = {
|
|
248
|
+
...canonicalPayload,
|
|
249
|
+
ownerAddress: this.signer.address,
|
|
250
|
+
signature: await this.signPayload(
|
|
251
|
+
decisionSignaturePayload(canonicalPayload)
|
|
252
|
+
),
|
|
253
|
+
};
|
|
254
|
+
const response = await this.request(
|
|
255
|
+
`/responses/${payload.responseId}/decision`,
|
|
256
|
+
body
|
|
257
|
+
);
|
|
258
|
+
return submissionResponseSchema.parse(response).response;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private async cancelTask(payload: CancelTaskPayload): Promise<TaskRecord> {
|
|
262
|
+
const body = {
|
|
263
|
+
...payload,
|
|
264
|
+
ownerAddress: this.signer.address,
|
|
265
|
+
signature: await this.signPayload(cancelTaskSignaturePayload(payload)),
|
|
266
|
+
};
|
|
267
|
+
const response = await this.request(
|
|
268
|
+
`/tasks/${payload.taskId}/cancel`,
|
|
269
|
+
body
|
|
270
|
+
);
|
|
271
|
+
return taskResponseSchema.parse(response).task;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private async queryTasks(payload: TaskQueryPayload): Promise<TaskRecord[]> {
|
|
275
|
+
const canonicalFilter = (payload.filter ?? {}) as NonNullable<
|
|
276
|
+
TaskQueryPayload["filter"]
|
|
277
|
+
>;
|
|
278
|
+
const pageSize = payload.pageSize ?? 50;
|
|
279
|
+
const pageNum = payload.pageNum ?? 0;
|
|
280
|
+
const body = {
|
|
281
|
+
filter: canonicalFilter,
|
|
282
|
+
sortBy: payload.sortBy ?? "created_at",
|
|
283
|
+
pageSize,
|
|
284
|
+
pageNum,
|
|
285
|
+
};
|
|
286
|
+
const response = await this.request("/tasks/query", body);
|
|
287
|
+
return tasksListSchema.parse(response).tasks;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
private async queryTaskResponses(
|
|
291
|
+
payload: TaskResponsesQueryPayload
|
|
292
|
+
): Promise<ResponseRecord[]> {
|
|
293
|
+
const pageSize = payload.pageSize ?? 50;
|
|
294
|
+
const pageNum = payload.pageNum ?? 0;
|
|
295
|
+
const canonicalPayload = {
|
|
296
|
+
taskId: payload.taskId,
|
|
297
|
+
workerAddress: payload.workerAddress,
|
|
298
|
+
pageSize,
|
|
299
|
+
pageNum,
|
|
300
|
+
};
|
|
301
|
+
const body = {
|
|
302
|
+
...canonicalPayload,
|
|
303
|
+
ownerAddress: this.signer.address,
|
|
304
|
+
signature: await this.signPayload(
|
|
305
|
+
taskResponsesQuerySignaturePayload({
|
|
306
|
+
...canonicalPayload,
|
|
307
|
+
ownerAddress: this.signer.address,
|
|
308
|
+
})
|
|
309
|
+
),
|
|
310
|
+
};
|
|
311
|
+
const response = await this.request("/tasks/responses/query", body);
|
|
312
|
+
return responsesListSchema.parse(response).responses;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
private async createSettlementSignature(
|
|
316
|
+
taskId: string,
|
|
317
|
+
workerAddress: `0x${string}`,
|
|
318
|
+
price: string,
|
|
319
|
+
tokenIdentifier: string
|
|
320
|
+
): Promise<Hex> {
|
|
321
|
+
const token = parseTokenIdentifier(tokenIdentifier);
|
|
322
|
+
const depositKey = taskDepositKey(taskId);
|
|
323
|
+
const digest = buildSettleDataHash(
|
|
324
|
+
this.contractAddress,
|
|
325
|
+
depositKey,
|
|
326
|
+
this.signer.address,
|
|
327
|
+
token.address,
|
|
328
|
+
workerAddress,
|
|
329
|
+
BigInt(price)
|
|
330
|
+
);
|
|
331
|
+
return this.signDigest(digest);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export class BountyAgentsWorkerPlugin extends BaseBountyPlugin {
|
|
336
|
+
constructor(signer: Signer, options: BountyAgentsPluginOptions) {
|
|
337
|
+
super(signer, options);
|
|
338
|
+
this.registerTool(
|
|
339
|
+
"bountyagents.worker.task.respond",
|
|
340
|
+
"Submit a response for an active task",
|
|
341
|
+
submitResponsePayloadSchema,
|
|
342
|
+
(input) => this.submitResponse(input)
|
|
343
|
+
);
|
|
344
|
+
this.registerTool(
|
|
345
|
+
"bountyagents.worker.response.query",
|
|
346
|
+
"List responses submitted by this worker",
|
|
347
|
+
workerResponsesQueryPayloadSchema,
|
|
348
|
+
(input) => this.queryWorkerResponses(input)
|
|
349
|
+
);
|
|
350
|
+
this.registerTool(
|
|
351
|
+
"bountyagents.worker.task.settle",
|
|
352
|
+
"Fetch settlement signature for an approved response",
|
|
353
|
+
settleTaskPayloadSchema,
|
|
354
|
+
(input) => this.settleTask(input)
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
private async submitResponse(
|
|
359
|
+
payload: SubmitResponsePayload
|
|
360
|
+
): Promise<ResponseRecord> {
|
|
361
|
+
const body = {
|
|
362
|
+
...payload,
|
|
363
|
+
workerAddress: this.signer.address,
|
|
364
|
+
signature: await this.signPayload(responseSignaturePayload(payload)),
|
|
365
|
+
};
|
|
366
|
+
const response = await this.request(
|
|
367
|
+
`/tasks/${payload.taskId}/responses`,
|
|
368
|
+
body
|
|
369
|
+
);
|
|
370
|
+
return submissionResponseSchema.parse(response).response;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
private async queryWorkerResponses(
|
|
374
|
+
payload: WorkerResponsesQueryPayload
|
|
375
|
+
): Promise<ResponseRecord[]> {
|
|
376
|
+
const pageSize = payload.pageSize ?? 50;
|
|
377
|
+
const pageNum = payload.pageNum ?? 0;
|
|
378
|
+
const canonicalPayload = {
|
|
379
|
+
taskId: payload.taskId,
|
|
380
|
+
pageSize,
|
|
381
|
+
pageNum,
|
|
382
|
+
};
|
|
383
|
+
const body = {
|
|
384
|
+
...canonicalPayload,
|
|
385
|
+
workerAddress: this.signer.address,
|
|
386
|
+
signature: await this.signPayload(
|
|
387
|
+
workerResponsesQuerySignaturePayload({
|
|
388
|
+
...canonicalPayload,
|
|
389
|
+
workerAddress: this.signer.address,
|
|
390
|
+
})
|
|
391
|
+
),
|
|
392
|
+
};
|
|
393
|
+
const response = await this.request("/workers/responses/query", body);
|
|
394
|
+
return responsesListSchema.parse(response).responses;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
private async settleTask(
|
|
398
|
+
payload: SettleTaskPayload
|
|
399
|
+
): Promise<{ task: TaskRecord; settlementSignature: string }> {
|
|
400
|
+
const canonicalPayload = { ...payload, workerAddress: this.signer.address };
|
|
401
|
+
const body = {
|
|
402
|
+
...canonicalPayload,
|
|
403
|
+
signature: await this.signPayload(
|
|
404
|
+
taskSettleSignaturePayload(canonicalPayload)
|
|
405
|
+
),
|
|
406
|
+
};
|
|
407
|
+
const response = await this.request(
|
|
408
|
+
`/tasks/${payload.taskId}/settle`,
|
|
409
|
+
body
|
|
410
|
+
);
|
|
411
|
+
return settleResponseSchema.parse(response);
|
|
412
|
+
}
|
|
413
|
+
}
|
package/src/signers.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Account, Address, Hex, hexToBytes } from 'viem';
|
|
2
|
+
import { privateKeyToAccount } from 'viem/accounts';
|
|
3
|
+
|
|
4
|
+
export interface Signer {
|
|
5
|
+
readonly address: Address;
|
|
6
|
+
signMessage(message: string): Promise<Hex>;
|
|
7
|
+
signDigest(digest: Hex): Promise<Hex>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class PrivateKeySigner implements Signer {
|
|
11
|
+
private readonly account: Account;
|
|
12
|
+
|
|
13
|
+
constructor(privateKey: Hex) {
|
|
14
|
+
this.account = privateKeyToAccount(privateKey);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
get address(): Address {
|
|
18
|
+
return this.account.address;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
signMessage(message: string): Promise<Hex> {
|
|
22
|
+
const signMessageFn = this.account.signMessage;
|
|
23
|
+
if (!signMessageFn) {
|
|
24
|
+
throw new Error('Account does not support signMessage');
|
|
25
|
+
}
|
|
26
|
+
return signMessageFn({ message });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
signDigest(digest: Hex): Promise<Hex> {
|
|
30
|
+
const signMessageFn = this.account.signMessage;
|
|
31
|
+
if (!signMessageFn) {
|
|
32
|
+
throw new Error('Account does not support signDigest');
|
|
33
|
+
}
|
|
34
|
+
return signMessageFn({ message: { raw: hexToBytes(digest) } });
|
|
35
|
+
}
|
|
36
|
+
}
|