@blindmarket/sdk 0.6.1 → 0.6.2
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/CHANGELOG.md +99 -0
- package/README.md +141 -40
- package/dist/executor/WorkerRuntime.d.ts +80 -18
- package/dist/executor/WorkerRuntime.js +306 -101
- package/dist/index.d.ts +101 -22
- package/dist/index.js +158 -14
- package/dist/tools/helpers.js +33 -11
- package/dist/types.d.ts +96 -9
- package/dist/worker/Worker.d.ts +5 -3
- package/dist/worker/Worker.js +5 -3
- package/package.json +9 -5
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import { ethers } from 'ethers';
|
|
2
|
-
import type { Address, Hex, RootHash, HealthStatus, PlatformStats, OpenTask, TaskDetail, CreateTaskTx, ExecutorProfile, RegisterExecutorInput, DeployedAgentInfo, AgentWalletInfo, ReputationInfo, LeaderboardEntry, StorageUploadResult, Message, AgentSearchResult, TaskTemplate, VerifyTaskInput,
|
|
2
|
+
import type { Address, Hex, RootHash, HealthStatus, PlatformStats, OpenTask, TaskDetail, CreateTaskTx, ExecutorProfile, RegisterExecutorInput, DeployedAgentInfo, AgentWalletInfo, ReputationInfo, LeaderboardEntry, StorageUploadResult, Message, AgentSearchResult, TaskTemplate, VerifyTaskInput, A2ATaskEntry, CreateAgentParams, CreateAgentResult, CreateTaskRequest } from './types.js';
|
|
3
3
|
export interface BlindMarketConfig {
|
|
4
4
|
/** Backend API base URL (default: https://api.blindmarket.xyz) */
|
|
5
5
|
apiBase?: string;
|
|
6
6
|
/** API key — shared AGENT_API_KEY or device-flow token */
|
|
7
7
|
apiKey: string;
|
|
8
|
+
/**
|
|
9
|
+
* Executor signer: the private key of the wallet that owns `apiKey`, plus
|
|
10
|
+
* the RPC(s) to broadcast `submitEvidence` on. Default for `createAgent()`
|
|
11
|
+
* and `deliverResult()`, and what enables the `submit_result` tool — tools
|
|
12
|
+
* never take a key as an argument. Stays in-process; never sent to the backend.
|
|
13
|
+
*/
|
|
14
|
+
executor?: DeliverSigner;
|
|
8
15
|
}
|
|
9
16
|
export interface DeployAgentParams {
|
|
10
17
|
name: string;
|
|
@@ -28,7 +35,18 @@ export interface DeployedAgent {
|
|
|
28
35
|
declare class ApiError extends Error {
|
|
29
36
|
status: number;
|
|
30
37
|
body?: unknown | undefined;
|
|
31
|
-
|
|
38
|
+
/** Backend error code (e.g. 'NEEDS_WRAP', 'NOT_SUBMITTED_ON_CHAIN'), when the envelope carried one. */
|
|
39
|
+
code?: string | undefined;
|
|
40
|
+
constructor(status: number, message: string, body?: unknown | undefined,
|
|
41
|
+
/** Backend error code (e.g. 'NEEDS_WRAP', 'NOT_SUBMITTED_ON_CHAIN'), when the envelope carried one. */
|
|
42
|
+
code?: string | undefined);
|
|
43
|
+
}
|
|
44
|
+
/** Per-chain RPC URLs for signing `submitEvidence` — a task is escrowed on exactly one chain. */
|
|
45
|
+
export interface DeliverSigner {
|
|
46
|
+
/** Private key of the executor wallet (the API key's owner) — `submitEvidence` is `onlyWorker`. */
|
|
47
|
+
privateKey: string;
|
|
48
|
+
/** RPC per chain. No default: a missing entry refuses the task's chain rather than guessing a network. */
|
|
49
|
+
rpcUrls: Partial<Record<string, string | undefined>>;
|
|
32
50
|
}
|
|
33
51
|
/**
|
|
34
52
|
* BlindMarket REST API client.
|
|
@@ -53,7 +71,10 @@ declare class ApiError extends Error {
|
|
|
53
71
|
export declare class BlindMarket {
|
|
54
72
|
private apiBase;
|
|
55
73
|
private apiKey;
|
|
74
|
+
private executor?;
|
|
56
75
|
constructor(config: BlindMarketConfig);
|
|
76
|
+
/** True when an executor signer was configured (see BlindMarketConfig.executor). */
|
|
77
|
+
get canSign(): boolean;
|
|
57
78
|
/**
|
|
58
79
|
* Tool definitions for AI agent frameworks. Access framework-specific formats
|
|
59
80
|
* via property — no need to remember adapter function names.
|
|
@@ -91,16 +112,11 @@ export declare class BlindMarket {
|
|
|
91
112
|
getTask(id: string): Promise<TaskDetail>;
|
|
92
113
|
/**
|
|
93
114
|
* Build an unsigned `createTask` transaction.
|
|
94
|
-
* You must sign and broadcast it with your wallet
|
|
115
|
+
* You must sign and broadcast it with your wallet (the API key's owner is
|
|
116
|
+
* the poster), then index it via `POST /api/v1/a2a/tasks/index` for A2A.
|
|
117
|
+
* The deadline is set on-chain as now + `duration` seconds.
|
|
95
118
|
*/
|
|
96
|
-
createTask(params:
|
|
97
|
-
agent: Address;
|
|
98
|
-
amount: string;
|
|
99
|
-
token: Address;
|
|
100
|
-
category: string;
|
|
101
|
-
locationZone: string;
|
|
102
|
-
deadline: number;
|
|
103
|
-
}): Promise<CreateTaskTx>;
|
|
119
|
+
createTask(params: CreateTaskRequest): Promise<CreateTaskTx>;
|
|
104
120
|
/**
|
|
105
121
|
* Build an unsigned `assignWorker` transaction.
|
|
106
122
|
*/
|
|
@@ -146,22 +162,48 @@ export declare class BlindMarket {
|
|
|
146
162
|
*/
|
|
147
163
|
deployAgent(params: DeployAgentParams): Promise<DeployedAgent>;
|
|
148
164
|
/**
|
|
149
|
-
* One-shot
|
|
150
|
-
* agent as an executor in the A2A marketplace with the generated wallet
|
|
151
|
-
* address and public key. Replaces the manual two-step flow of generating a
|
|
152
|
-
* wallet, then calling registerExecutor().
|
|
165
|
+
* One-shot executor registration in the A2A marketplace.
|
|
153
166
|
*
|
|
154
|
-
* The
|
|
167
|
+
* The registered executor is ALWAYS the wallet that owns the API key — the
|
|
168
|
+
* backend takes the address from auth, never from the request. Briefs are
|
|
169
|
+
* wrapped to the public key registered here, and `submitEvidence` is built
|
|
170
|
+
* for the owner address. So pass `privateKey` (or set
|
|
171
|
+
* `BlindMarketConfig.executor`) — the OWNER wallet's key: its uncompressed
|
|
172
|
+
* public key is registered, the key never leaves this process, and this
|
|
173
|
+
* throws 409 OWNER_MISMATCH — BEFORE registering anything — when the key is
|
|
174
|
+
* not the API key's owner (checked through `whoami()`).
|
|
175
|
+
*
|
|
176
|
+
* Without a key a random secp256k1 wallet is generated and its private key
|
|
177
|
+
* returned **once** (store it securely). That wallet can decrypt briefs, but
|
|
178
|
+
* it is not the registered executor address, so it cannot sign
|
|
179
|
+
* `submitEvidence` for tasks the owner accepts — use it only when delivery
|
|
180
|
+
* goes through another signer (e.g. the backend relay).
|
|
155
181
|
*
|
|
156
182
|
* @example
|
|
157
183
|
* const { executor, wallet } = await bb.createAgent({
|
|
184
|
+
* privateKey: process.env.EXECUTOR_PRIVATE_KEY!, // the API key owner's wallet (or set BlindMarketConfig.executor)
|
|
158
185
|
* displayName: 'DataBot',
|
|
159
186
|
* capabilities: [AgentCap.DATA_PROCESSING, AgentCap.WEB_RESEARCH],
|
|
160
187
|
* minReward: '1000000', // 1 USDC (the payment token's smallest unit; USDC has 6 decimals)
|
|
161
188
|
* });
|
|
162
|
-
* console.log(`
|
|
189
|
+
* console.log(`Registered executor ${executor.address}`);
|
|
163
190
|
*/
|
|
164
191
|
createAgent(params: CreateAgentParams): Promise<CreateAgentResult>;
|
|
192
|
+
/**
|
|
193
|
+
* The wallet this API key authenticates as (`GET /api/v1/api-keys/whoami`).
|
|
194
|
+
* /a2a/register and /accept act for `address`. A legacy shared
|
|
195
|
+
* AGENT_API_KEY resolves to the non-wallet principal `"agent"`.
|
|
196
|
+
*/
|
|
197
|
+
whoami(): Promise<{
|
|
198
|
+
address: string;
|
|
199
|
+
addresses?: string[];
|
|
200
|
+
}>;
|
|
201
|
+
/**
|
|
202
|
+
* Throws 409 OWNER_MISMATCH, without side effects, when `address` is not
|
|
203
|
+
* the API key's owner. Returns false only when the backend has no whoami
|
|
204
|
+
* route (404 / a non-JSON 404 page) and nothing could be checked.
|
|
205
|
+
*/
|
|
206
|
+
private assertOwnerKey;
|
|
165
207
|
/** List deployed agents, optionally filtered by owner address. */
|
|
166
208
|
listAgents(ownerAddress?: string): Promise<DeployedAgentInfo[]>;
|
|
167
209
|
/** Get a single deployed agent by ID. */
|
|
@@ -187,7 +229,12 @@ export declare class BlindMarket {
|
|
|
187
229
|
tools: object[];
|
|
188
230
|
minReward: string;
|
|
189
231
|
}>): Promise<DeployedAgentInfo>;
|
|
190
|
-
/**
|
|
232
|
+
/**
|
|
233
|
+
* Register as an A2A agent executor (worker-side). The executor address is
|
|
234
|
+
* the API key's owner wallet (any `address` sent is ignored). `publicKey`
|
|
235
|
+
* must be uncompressed secp256k1 hex, 130 chars, leading `04`, no 0x —
|
|
236
|
+
* `new ethers.Wallet(pk).signingKey.publicKey.slice(2)`, NOT `wallet.publicKey`.
|
|
237
|
+
*/
|
|
191
238
|
registerExecutor(params: RegisterExecutorInput): Promise<{
|
|
192
239
|
agent: ExecutorProfile;
|
|
193
240
|
}>;
|
|
@@ -199,12 +246,17 @@ export declare class BlindMarket {
|
|
|
199
246
|
getExecutorProfile(): Promise<{
|
|
200
247
|
agent: ExecutorProfile;
|
|
201
248
|
}>;
|
|
202
|
-
/**
|
|
249
|
+
/**
|
|
250
|
+
* Browse A2A tasks available for execution. Each entry is `{ meta, state }`
|
|
251
|
+
* — the id and status live on `state` (`entry.state.taskId`), the chain and
|
|
252
|
+
* deadline on `meta`.
|
|
253
|
+
*/
|
|
203
254
|
browseA2ATasks(params?: {
|
|
204
255
|
capabilities?: string[];
|
|
205
256
|
minReputation?: number;
|
|
206
257
|
}): Promise<{
|
|
207
|
-
tasks:
|
|
258
|
+
tasks: A2ATaskEntry[];
|
|
259
|
+
total?: number;
|
|
208
260
|
}>;
|
|
209
261
|
/** Register intent to accept a task (bid). */
|
|
210
262
|
bidOnTask(taskId: string): Promise<void>;
|
|
@@ -266,13 +318,40 @@ export declare class BlindMarket {
|
|
|
266
318
|
};
|
|
267
319
|
reconciled?: boolean;
|
|
268
320
|
}>;
|
|
321
|
+
/**
|
|
322
|
+
* Rebuild the unsigned `submitEvidence` for a task stranded in 'submitted'
|
|
323
|
+
* — `/submit` flips the state when the tx is BUILT, so a crash before the
|
|
324
|
+
* broadcast leaves `finalize()` failing with NOT_SUBMITTED_ON_CHAIN and
|
|
325
|
+
* `submitResult()` refusing with INVALID_STATE. Sign + broadcast the
|
|
326
|
+
* returned tx on `chain`, then call `finalize()`. 409 ALREADY_SUBMITTED
|
|
327
|
+
* means the evidence did land — just call `finalize()`.
|
|
328
|
+
*/
|
|
329
|
+
rebroadcast(taskId: string): Promise<{
|
|
330
|
+
taskId: string;
|
|
331
|
+
onChainTaskId?: string;
|
|
332
|
+
/** A string, not a union — see submitResult(). */
|
|
333
|
+
chain?: string;
|
|
334
|
+
evidenceHash?: Hex;
|
|
335
|
+
unsignedSubmitEvidence?: Record<string, unknown> | null;
|
|
336
|
+
}>;
|
|
337
|
+
/**
|
|
338
|
+
* Deliver a result end to end: `submitResult()` → sign + broadcast the
|
|
339
|
+
* unsigned `submitEvidence` on the chain the backend names → `finalize()`.
|
|
340
|
+
* Safe to re-call on a task stranded in 'submitted': INVALID_STATE at submit
|
|
341
|
+
* and NOT_SUBMITTED_ON_CHAIN at finalize both heal through `rebroadcast()`.
|
|
342
|
+
*/
|
|
343
|
+
deliverResult(taskId: string, resultData: Record<string, unknown>, signerOverride?: DeliverSigner): Promise<Awaited<ReturnType<BlindMarket['finalize']>> & {
|
|
344
|
+
submitTxHash?: string;
|
|
345
|
+
}>;
|
|
269
346
|
/** Get tasks posted by the authenticated user. */
|
|
270
347
|
getPostedTasks(): Promise<{
|
|
271
|
-
tasks:
|
|
348
|
+
tasks: A2ATaskEntry[];
|
|
349
|
+
total?: number;
|
|
272
350
|
}>;
|
|
273
351
|
/** Get tasks executed by the authenticated user. */
|
|
274
352
|
getExecutions(address?: string): Promise<{
|
|
275
|
-
|
|
353
|
+
executions: A2ATaskEntry[];
|
|
354
|
+
total: number;
|
|
276
355
|
}>;
|
|
277
356
|
/**
|
|
278
357
|
* Trigger TEE / AI verification for a task.
|
package/dist/index.js
CHANGED
|
@@ -3,10 +3,14 @@ import { ethers } from 'ethers';
|
|
|
3
3
|
class ApiError extends Error {
|
|
4
4
|
status;
|
|
5
5
|
body;
|
|
6
|
-
|
|
6
|
+
code;
|
|
7
|
+
constructor(status, message, body,
|
|
8
|
+
/** Backend error code (e.g. 'NEEDS_WRAP', 'NOT_SUBMITTED_ON_CHAIN'), when the envelope carried one. */
|
|
9
|
+
code) {
|
|
7
10
|
super(message);
|
|
8
11
|
this.status = status;
|
|
9
12
|
this.body = body;
|
|
13
|
+
this.code = code;
|
|
10
14
|
this.name = 'ApiError';
|
|
11
15
|
}
|
|
12
16
|
}
|
|
@@ -34,9 +38,15 @@ class ApiError extends Error {
|
|
|
34
38
|
export class BlindMarket {
|
|
35
39
|
apiBase;
|
|
36
40
|
apiKey;
|
|
41
|
+
executor;
|
|
37
42
|
constructor(config) {
|
|
38
43
|
this.apiBase = config.apiBase ?? 'https://api.blindmarket.xyz';
|
|
39
44
|
this.apiKey = config.apiKey;
|
|
45
|
+
this.executor = config.executor;
|
|
46
|
+
}
|
|
47
|
+
/** True when an executor signer was configured (see BlindMarketConfig.executor). */
|
|
48
|
+
get canSign() {
|
|
49
|
+
return !!this.executor;
|
|
40
50
|
}
|
|
41
51
|
// ── Tools ─────────────────────────────────────────────────────────────────
|
|
42
52
|
/**
|
|
@@ -76,7 +86,7 @@ export class BlindMarket {
|
|
|
76
86
|
});
|
|
77
87
|
const json = await res.json();
|
|
78
88
|
if (!json.success) {
|
|
79
|
-
throw new ApiError(res.status, json.error?.message ?? `HTTP ${res.status}`, json);
|
|
89
|
+
throw new ApiError(res.status, json.error?.message ?? `HTTP ${res.status}`, json, json.error?.code);
|
|
80
90
|
}
|
|
81
91
|
return json.data;
|
|
82
92
|
}
|
|
@@ -101,7 +111,9 @@ export class BlindMarket {
|
|
|
101
111
|
}
|
|
102
112
|
/**
|
|
103
113
|
* Build an unsigned `createTask` transaction.
|
|
104
|
-
* You must sign and broadcast it with your wallet
|
|
114
|
+
* You must sign and broadcast it with your wallet (the API key's owner is
|
|
115
|
+
* the poster), then index it via `POST /api/v1/a2a/tasks/index` for A2A.
|
|
116
|
+
* The deadline is set on-chain as now + `duration` seconds.
|
|
105
117
|
*/
|
|
106
118
|
async createTask(params) {
|
|
107
119
|
return this.req('POST', '/api/v1/tasks', params);
|
|
@@ -151,28 +163,42 @@ export class BlindMarket {
|
|
|
151
163
|
return this.req('POST', '/api/v1/agents/deploy', params);
|
|
152
164
|
}
|
|
153
165
|
/**
|
|
154
|
-
* One-shot
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
166
|
+
* One-shot executor registration in the A2A marketplace.
|
|
167
|
+
*
|
|
168
|
+
* The registered executor is ALWAYS the wallet that owns the API key — the
|
|
169
|
+
* backend takes the address from auth, never from the request. Briefs are
|
|
170
|
+
* wrapped to the public key registered here, and `submitEvidence` is built
|
|
171
|
+
* for the owner address. So pass `privateKey` (or set
|
|
172
|
+
* `BlindMarketConfig.executor`) — the OWNER wallet's key: its uncompressed
|
|
173
|
+
* public key is registered, the key never leaves this process, and this
|
|
174
|
+
* throws 409 OWNER_MISMATCH — BEFORE registering anything — when the key is
|
|
175
|
+
* not the API key's owner (checked through `whoami()`).
|
|
158
176
|
*
|
|
159
|
-
*
|
|
177
|
+
* Without a key a random secp256k1 wallet is generated and its private key
|
|
178
|
+
* returned **once** (store it securely). That wallet can decrypt briefs, but
|
|
179
|
+
* it is not the registered executor address, so it cannot sign
|
|
180
|
+
* `submitEvidence` for tasks the owner accepts — use it only when delivery
|
|
181
|
+
* goes through another signer (e.g. the backend relay).
|
|
160
182
|
*
|
|
161
183
|
* @example
|
|
162
184
|
* const { executor, wallet } = await bb.createAgent({
|
|
185
|
+
* privateKey: process.env.EXECUTOR_PRIVATE_KEY!, // the API key owner's wallet (or set BlindMarketConfig.executor)
|
|
163
186
|
* displayName: 'DataBot',
|
|
164
187
|
* capabilities: [AgentCap.DATA_PROCESSING, AgentCap.WEB_RESEARCH],
|
|
165
188
|
* minReward: '1000000', // 1 USDC (the payment token's smallest unit; USDC has 6 decimals)
|
|
166
189
|
* });
|
|
167
|
-
* console.log(`
|
|
190
|
+
* console.log(`Registered executor ${executor.address}`);
|
|
168
191
|
*/
|
|
169
192
|
async createAgent(params) {
|
|
170
|
-
|
|
193
|
+
// With a key (params.privateKey, or BlindMarketConfig.executor) the agent
|
|
194
|
+
// IS the API key owner's wallet, the only one that can settle. Without one
|
|
195
|
+
// a random wallet is generated, as before — see the JSDoc for its limits.
|
|
196
|
+
const privateKey = params.privateKey ?? this.executor?.privateKey;
|
|
197
|
+
const wallet = privateKey ? new ethers.Wallet(privateKey) : ethers.Wallet.createRandom();
|
|
171
198
|
// The uncompressed key (0x04…): /register requires it, and posters wrap
|
|
172
199
|
// brief keys to it. `wallet.publicKey` is the compressed form in ethers v6.
|
|
173
200
|
const publicKey = wallet.signingKey.publicKey;
|
|
174
201
|
const executor = {
|
|
175
|
-
address: wallet.address,
|
|
176
202
|
displayName: params.displayName,
|
|
177
203
|
capabilities: params.capabilities,
|
|
178
204
|
publicKey: publicKey.slice(2), // strip 0x prefix — backend expects raw hex
|
|
@@ -182,7 +208,17 @@ export class BlindMarket {
|
|
|
182
208
|
preferredCapabilities: params.preferredCapabilities,
|
|
183
209
|
supportedChains: params.supportedChains,
|
|
184
210
|
};
|
|
211
|
+
// Only when the caller supplied the key: they are claiming to be the owner.
|
|
212
|
+
// Checked BEFORE /register, which upserts `publicKey` over the owner's
|
|
213
|
+
// record — a mismatch found afterwards has already redirected every new
|
|
214
|
+
// brief to a key whose wallet cannot sign submitEvidence.
|
|
215
|
+
const ownerChecked = privateKey ? await this.assertOwnerKey(wallet.address) : false;
|
|
185
216
|
const result = await this.registerExecutor(executor);
|
|
217
|
+
// Backends without /api-keys/whoami could not be checked up front.
|
|
218
|
+
if (privateKey && !ownerChecked && result.agent.address.toLowerCase() !== wallet.address.toLowerCase()) {
|
|
219
|
+
throw new ApiError(409, `Registered executor is ${result.agent.address} (the API key's owner) but privateKey belongs to ${wallet.address}. ` +
|
|
220
|
+
'This backend has no /api-keys/whoami, so the mismatch could only be seen after registering: briefs are now wrapped to a key whose wallet cannot sign submitEvidence — re-run with the owner wallet\'s key, or mint an API key signed in as this wallet.', undefined, 'OWNER_MISMATCH');
|
|
221
|
+
}
|
|
186
222
|
return {
|
|
187
223
|
executor: result.agent,
|
|
188
224
|
wallet: {
|
|
@@ -192,6 +228,35 @@ export class BlindMarket {
|
|
|
192
228
|
},
|
|
193
229
|
};
|
|
194
230
|
}
|
|
231
|
+
/**
|
|
232
|
+
* The wallet this API key authenticates as (`GET /api/v1/api-keys/whoami`).
|
|
233
|
+
* /a2a/register and /accept act for `address`. A legacy shared
|
|
234
|
+
* AGENT_API_KEY resolves to the non-wallet principal `"agent"`.
|
|
235
|
+
*/
|
|
236
|
+
async whoami() {
|
|
237
|
+
return this.req('GET', '/api/v1/api-keys/whoami');
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Throws 409 OWNER_MISMATCH, without side effects, when `address` is not
|
|
241
|
+
* the API key's owner. Returns false only when the backend has no whoami
|
|
242
|
+
* route (404 / a non-JSON 404 page) and nothing could be checked.
|
|
243
|
+
*/
|
|
244
|
+
async assertOwnerKey(address) {
|
|
245
|
+
let owner;
|
|
246
|
+
try {
|
|
247
|
+
owner = (await this.whoami()).address;
|
|
248
|
+
}
|
|
249
|
+
catch (err) {
|
|
250
|
+
if (err instanceof SyntaxError || (err instanceof ApiError && err.status === 404))
|
|
251
|
+
return false;
|
|
252
|
+
throw err;
|
|
253
|
+
}
|
|
254
|
+
if (typeof owner !== 'string' || owner.toLowerCase() !== address.toLowerCase()) {
|
|
255
|
+
throw new ApiError(409, `This API key belongs to ${owner} but privateKey belongs to ${address}. Nothing was registered. ` +
|
|
256
|
+
"The executor is always the API key's owner, and only that wallet can sign submitEvidence — use the owner wallet's key, or mint an API key signed in as this wallet.", undefined, 'OWNER_MISMATCH');
|
|
257
|
+
}
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
195
260
|
/** List deployed agents, optionally filtered by owner address. */
|
|
196
261
|
async listAgents(ownerAddress) {
|
|
197
262
|
const qs = ownerAddress ? `?owner=${ownerAddress}` : '';
|
|
@@ -229,7 +294,12 @@ export class BlindMarket {
|
|
|
229
294
|
return this.req('PATCH', `/api/v1/agents/${id}`, patch);
|
|
230
295
|
}
|
|
231
296
|
// ── A2A executor registration ───────────────────────────────────────────
|
|
232
|
-
/**
|
|
297
|
+
/**
|
|
298
|
+
* Register as an A2A agent executor (worker-side). The executor address is
|
|
299
|
+
* the API key's owner wallet (any `address` sent is ignored). `publicKey`
|
|
300
|
+
* must be uncompressed secp256k1 hex, 130 chars, leading `04`, no 0x —
|
|
301
|
+
* `new ethers.Wallet(pk).signingKey.publicKey.slice(2)`, NOT `wallet.publicKey`.
|
|
302
|
+
*/
|
|
233
303
|
async registerExecutor(params) {
|
|
234
304
|
return this.req('POST', '/api/v1/a2a/register', params);
|
|
235
305
|
}
|
|
@@ -243,10 +313,14 @@ export class BlindMarket {
|
|
|
243
313
|
return this.req('GET', '/api/v1/a2a/profile');
|
|
244
314
|
}
|
|
245
315
|
// ── A2A task lifecycle ───────────────────────────────────────────────────
|
|
246
|
-
/**
|
|
316
|
+
/**
|
|
317
|
+
* Browse A2A tasks available for execution. Each entry is `{ meta, state }`
|
|
318
|
+
* — the id and status live on `state` (`entry.state.taskId`), the chain and
|
|
319
|
+
* deadline on `meta`.
|
|
320
|
+
*/
|
|
247
321
|
async browseA2ATasks(params) {
|
|
248
322
|
const qs = new URLSearchParams();
|
|
249
|
-
if (params?.capabilities)
|
|
323
|
+
if (params?.capabilities?.length)
|
|
250
324
|
qs.set('capabilities', params.capabilities.join(','));
|
|
251
325
|
if (params?.minReputation != null)
|
|
252
326
|
qs.set('minReputation', String(params.minReputation));
|
|
@@ -287,6 +361,76 @@ export class BlindMarket {
|
|
|
287
361
|
async finalize(taskId) {
|
|
288
362
|
return this.req('POST', `/api/v1/a2a/tasks/${taskId}/finalize`);
|
|
289
363
|
}
|
|
364
|
+
/**
|
|
365
|
+
* Rebuild the unsigned `submitEvidence` for a task stranded in 'submitted'
|
|
366
|
+
* — `/submit` flips the state when the tx is BUILT, so a crash before the
|
|
367
|
+
* broadcast leaves `finalize()` failing with NOT_SUBMITTED_ON_CHAIN and
|
|
368
|
+
* `submitResult()` refusing with INVALID_STATE. Sign + broadcast the
|
|
369
|
+
* returned tx on `chain`, then call `finalize()`. 409 ALREADY_SUBMITTED
|
|
370
|
+
* means the evidence did land — just call `finalize()`.
|
|
371
|
+
*/
|
|
372
|
+
async rebroadcast(taskId) {
|
|
373
|
+
return this.req('POST', `/api/v1/a2a/tasks/${taskId}/rebroadcast`);
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Deliver a result end to end: `submitResult()` → sign + broadcast the
|
|
377
|
+
* unsigned `submitEvidence` on the chain the backend names → `finalize()`.
|
|
378
|
+
* Safe to re-call on a task stranded in 'submitted': INVALID_STATE at submit
|
|
379
|
+
* and NOT_SUBMITTED_ON_CHAIN at finalize both heal through `rebroadcast()`.
|
|
380
|
+
*/
|
|
381
|
+
async deliverResult(taskId, resultData, signerOverride) {
|
|
382
|
+
const signer = signerOverride ?? this.executor;
|
|
383
|
+
if (!signer) {
|
|
384
|
+
throw new ApiError(400, 'deliverResult() needs a signer — pass one, or set BlindMarketConfig.executor. submitEvidence is onlyWorker, so the backend cannot broadcast it for you.');
|
|
385
|
+
}
|
|
386
|
+
const send = async (built) => {
|
|
387
|
+
if (!built.unsignedSubmitEvidence)
|
|
388
|
+
return undefined;
|
|
389
|
+
// Absent `chain` = a backend older than the field, where every task is on 0G.
|
|
390
|
+
// Any other name must have its own RPC entry: signing an unknown chain's
|
|
391
|
+
// tx on the 0G RPC would target the wrong escrow.
|
|
392
|
+
const chain = built.chain ?? '0g';
|
|
393
|
+
const rpc = signer.rpcUrls[chain];
|
|
394
|
+
if (!rpc) {
|
|
395
|
+
throw new Error(`task ${taskId} is escrowed on ${chain} but no RPC is configured for it — set rpcUrls.${chain}`);
|
|
396
|
+
}
|
|
397
|
+
// The tx carries chainId, so a wrong RPC fails at ethers instead of
|
|
398
|
+
// landing on the wrong network.
|
|
399
|
+
const wallet = new ethers.Wallet(signer.privateKey, new ethers.JsonRpcProvider(rpc));
|
|
400
|
+
const tx = await wallet.sendTransaction(built.unsignedSubmitEvidence);
|
|
401
|
+
await tx.wait();
|
|
402
|
+
return tx.hash;
|
|
403
|
+
};
|
|
404
|
+
const healStranded = async () => {
|
|
405
|
+
try {
|
|
406
|
+
return await send(await this.rebroadcast(taskId));
|
|
407
|
+
}
|
|
408
|
+
catch (err) {
|
|
409
|
+
// Evidence is already on-chain — nothing to broadcast, go finalize.
|
|
410
|
+
if (err instanceof ApiError && err.code === 'ALREADY_SUBMITTED')
|
|
411
|
+
return undefined;
|
|
412
|
+
throw err;
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
let submitTxHash;
|
|
416
|
+
try {
|
|
417
|
+
submitTxHash = await send(await this.submitResult(taskId, resultData));
|
|
418
|
+
}
|
|
419
|
+
catch (err) {
|
|
420
|
+
if (!(err instanceof ApiError && err.code === 'INVALID_STATE'))
|
|
421
|
+
throw err;
|
|
422
|
+
submitTxHash = await healStranded();
|
|
423
|
+
}
|
|
424
|
+
try {
|
|
425
|
+
return { ...(await this.finalize(taskId)), submitTxHash };
|
|
426
|
+
}
|
|
427
|
+
catch (err) {
|
|
428
|
+
if (!(err instanceof ApiError && err.code === 'NOT_SUBMITTED_ON_CHAIN'))
|
|
429
|
+
throw err;
|
|
430
|
+
submitTxHash = (await healStranded()) ?? submitTxHash;
|
|
431
|
+
return { ...(await this.finalize(taskId)), submitTxHash };
|
|
432
|
+
}
|
|
433
|
+
}
|
|
290
434
|
/** Get tasks posted by the authenticated user. */
|
|
291
435
|
async getPostedTasks() {
|
|
292
436
|
return this.req('GET', '/api/v1/a2a/tasks/posted');
|
package/dist/tools/helpers.js
CHANGED
|
@@ -23,7 +23,7 @@ export function kit(name, description, all, names) {
|
|
|
23
23
|
return { name, description, tools: all.filter((t) => namesSet.has(t.definition.function.name)) };
|
|
24
24
|
}
|
|
25
25
|
export function createBlindMarketTools(bb) {
|
|
26
|
-
|
|
26
|
+
const all = [
|
|
27
27
|
tool(bb, 'list_open_tasks', 'List open tasks available for assignment', {}, async () => {
|
|
28
28
|
return bb.listTasks();
|
|
29
29
|
}),
|
|
@@ -36,7 +36,7 @@ export function createBlindMarketTools(bb) {
|
|
|
36
36
|
}, async (a) => {
|
|
37
37
|
return bb.searchAgents(a);
|
|
38
38
|
}),
|
|
39
|
-
tool(bb, 'create_agent',
|
|
39
|
+
tool(bb, 'create_agent', "Register the API key's owner wallet as an A2A executor, using the executor key configured on the client (BlindMarketConfig.executor). With no key configured, a random wallet is generated and its private key returned once — that wallet can decrypt briefs but cannot sign submitEvidence for the owner.", {
|
|
40
40
|
displayName: str('Display name for the agent'),
|
|
41
41
|
capabilities: arr('List of capabilities', str('Capability', CAP_ENUM)),
|
|
42
42
|
minReward: str("Minimum reward per task, as an integer in the payment token's smallest unit (USDC: 6 decimals) (optional)"),
|
|
@@ -44,21 +44,27 @@ export function createBlindMarketTools(bb) {
|
|
|
44
44
|
agentCardUrl: str('Agent card URL for marketplace display (optional)'),
|
|
45
45
|
mcpEndpointUrl: str('MCP endpoint URL (optional)'),
|
|
46
46
|
}, async (a) => {
|
|
47
|
-
|
|
47
|
+
// A configured key comes from the client config, never from tool
|
|
48
|
+
// arguments, and is never echoed back into the model's context. A
|
|
49
|
+
// generated one has to be returned: this is the only place it exists.
|
|
50
|
+
const { executor, wallet } = await bb.createAgent(a);
|
|
51
|
+
if (!bb.canSign)
|
|
52
|
+
return { executor, wallet };
|
|
53
|
+
return { executor, wallet: { address: wallet.address, publicKey: wallet.publicKey } };
|
|
48
54
|
}, ['displayName', 'capabilities']),
|
|
49
55
|
tool(bb, 'register_as_executor', 'Register as an A2A executor to receive task offers', {
|
|
50
|
-
address: str('
|
|
56
|
+
address: str("Ignored — the executor is always the API key's owner wallet (optional)"),
|
|
51
57
|
displayName: str('Human-readable display name'),
|
|
52
58
|
capabilities: arr('List of capabilities', str('Capability', CAP_ENUM)),
|
|
53
|
-
publicKey: str('Your uncompressed secp256k1 public key'),
|
|
59
|
+
publicKey: str('Your uncompressed secp256k1 public key: 130 hex chars, leading 04, no 0x prefix'),
|
|
54
60
|
minReward: str("Minimum reward, as an integer in the payment token's smallest unit (USDC: 6 decimals) (optional)"),
|
|
55
61
|
preferredCapabilities: arr('Preferred subset of capabilities (optional)', str('Capability', CAP_ENUM)),
|
|
56
62
|
// No enum: the backend validates the list, and a newer backend may accept
|
|
57
63
|
// a chain this SDK version doesn't know.
|
|
58
|
-
supportedChains: arr("Settlement chains you can sign submitEvidence on, e.g. ['0g', 'base'].
|
|
64
|
+
supportedChains: arr("Settlement chains you can sign submitEvidence on, e.g. ['0g', 'base']. Stored on your executor record as a declaration; the backend does not filter offers by it, so check a task's chain before accepting (optional)", str('Chain slug')),
|
|
59
65
|
}, async (a) => {
|
|
60
66
|
return bb.registerExecutor(a);
|
|
61
|
-
}, ['
|
|
67
|
+
}, ['displayName', 'capabilities', 'publicKey']),
|
|
62
68
|
tool(bb, 'browse_a2a_tasks', 'Browse tasks available for A2A execution', {
|
|
63
69
|
capabilities: arr('Required capabilities filter'),
|
|
64
70
|
minReputation: num('Minimum reputation filter'),
|
|
@@ -71,16 +77,19 @@ export function createBlindMarketTools(bb) {
|
|
|
71
77
|
await bb.bidOnTask(a.taskId);
|
|
72
78
|
return { success: true };
|
|
73
79
|
}, ['taskId']),
|
|
74
|
-
tool(bb, 'accept_task', '
|
|
80
|
+
tool(bb, 'accept_task', 'Claim an open task (assigns you on-chain) and get the rootHash + wrapped AES key. On NEEDS_WRAP, call bid_on_task and retry once the poster has wrapped the key to you.', {
|
|
75
81
|
taskId: str('Task ID'),
|
|
76
82
|
}, async (a) => {
|
|
77
83
|
return bb.acceptTask(a.taskId);
|
|
78
84
|
}, ['taskId']),
|
|
79
|
-
|
|
80
|
-
|
|
85
|
+
// Completes the WHOLE delivery: /submit only builds an unsigned
|
|
86
|
+
// submitEvidence and flips state to 'submitted', so a tool that stopped
|
|
87
|
+
// there stranded the task. Needs BlindMarketConfig.executor to sign.
|
|
88
|
+
tool(bb, 'submit_result', "Deliver the result for an accepted task: submits it, signs + broadcasts submitEvidence from the executor wallet, then finalizes so verification can release the escrow. Safe to re-call — a task stranded in 'submitted' is healed via rebroadcast.", {
|
|
89
|
+
taskId: str('Task ID (0x task hash)'),
|
|
81
90
|
output: str('Result output text'),
|
|
82
91
|
}, async (a) => {
|
|
83
|
-
return bb.
|
|
92
|
+
return bb.deliverResult(a.taskId, { output: a.output });
|
|
84
93
|
}, ['taskId', 'output']),
|
|
85
94
|
tool(bb, 'deploy_agent', 'Deploy a new AI agent on BlindMarket', {
|
|
86
95
|
name: str('Agent name'),
|
|
@@ -162,7 +171,20 @@ export function createBlindMarketTools(bb) {
|
|
|
162
171
|
return bb.getUnreadCount();
|
|
163
172
|
}),
|
|
164
173
|
];
|
|
174
|
+
// submit_result signs a transaction; without a configured signer it could
|
|
175
|
+
// only strand tasks, so it is not offered at all.
|
|
176
|
+
if (bb.canSign)
|
|
177
|
+
return all;
|
|
178
|
+
// Said once per process: up to 0.5.x the tool was always present, and a
|
|
179
|
+
// model that is simply never offered it fails quietly.
|
|
180
|
+
if (!warnedNoSubmitResult) {
|
|
181
|
+
warnedNoSubmitResult = true;
|
|
182
|
+
console.warn('[@blindmarket/sdk] the submit_result tool is not offered: the client has no executor signer. ' +
|
|
183
|
+
'Pass `executor: { privateKey, rpcUrls }` to new BlindMarket() to enable it (see CHANGELOG 0.6.0).');
|
|
184
|
+
}
|
|
185
|
+
return all.filter((t) => t.definition.function.name !== 'submit_result');
|
|
165
186
|
}
|
|
187
|
+
let warnedNoSubmitResult = false;
|
|
166
188
|
export function createTaskTools(bb) {
|
|
167
189
|
return kit('tasks', 'Browse and manage tasks', createBlindMarketTools(bb), [
|
|
168
190
|
'list_open_tasks', 'get_task', 'bid_on_task', 'accept_task', 'submit_result',
|