@blindmarket/sdk 0.1.8 → 0.5.0

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.
Files changed (89) hide show
  1. package/dist/executor/WorkerRuntime.d.ts +21 -0
  2. package/dist/executor/WorkerRuntime.js +56 -27
  3. package/dist/index.d.ts +75 -9
  4. package/dist/index.js +54 -5
  5. package/dist/tools/helpers.js +14 -5
  6. package/dist/types.d.ts +7 -2
  7. package/package.json +8 -1
  8. package/biome.json +0 -19
  9. package/examples/agent-demo.ts +0 -121
  10. package/scripts/gen-crypto-fixtures.mjs +0 -72
  11. package/src/agent/Agent.ts +0 -122
  12. package/src/agent/index.ts +0 -1
  13. package/src/api/index.ts +0 -1
  14. package/src/chain/BlindEscrowClient.ts +0 -196
  15. package/src/chain/BlindReputationClient.ts +0 -60
  16. package/src/chain/ChainClient.ts +0 -65
  17. package/src/chain/IBlindMarketChain.ts +0 -109
  18. package/src/chain/TaskRegistryClient.ts +0 -91
  19. package/src/chain/abi/BlindEscrow.json +0 -1113
  20. package/src/chain/abi/BlindReputation.json +0 -447
  21. package/src/chain/abi/TaskRegistry.json +0 -586
  22. package/src/chain/abi/index.ts +0 -5
  23. package/src/chain/createChainAdapter.ts +0 -40
  24. package/src/chain/domain-types.ts +0 -92
  25. package/src/chain/errors.ts +0 -43
  26. package/src/chain/evm/EVMChainAdapter.ts +0 -212
  27. package/src/chain/evm/index.ts +0 -1
  28. package/src/chain/index.ts +0 -12
  29. package/src/compute/index.ts +0 -1
  30. package/src/crypto/aes.ts +0 -104
  31. package/src/crypto/constants.ts +0 -7
  32. package/src/crypto/ecies.ts +0 -60
  33. package/src/crypto/envelope.ts +0 -131
  34. package/src/crypto/hkdf.ts +0 -45
  35. package/src/crypto/index.ts +0 -6
  36. package/src/crypto/keys.ts +0 -56
  37. package/src/errors/BlindMarketError.ts +0 -38
  38. package/src/errors/codes.ts +0 -67
  39. package/src/errors/index.ts +0 -3
  40. package/src/errors/subclasses.ts +0 -59
  41. package/src/events/EventBus.ts +0 -45
  42. package/src/events/index.ts +0 -2
  43. package/src/events/types.ts +0 -13
  44. package/src/executor/WorkerRuntime.ts +0 -338
  45. package/src/executor/index.ts +0 -8
  46. package/src/index.ts +0 -636
  47. package/src/keystore/InMemoryKeyStore.ts +0 -146
  48. package/src/keystore/KeyStore.ts +0 -19
  49. package/src/keystore/index.ts +0 -2
  50. package/src/network/index.ts +0 -3
  51. package/src/network/presets.ts +0 -66
  52. package/src/network/resolve.ts +0 -31
  53. package/src/network/types.ts +0 -20
  54. package/src/signer/EthersSigner.ts +0 -82
  55. package/src/signer/PrivateKeySigner.ts +0 -19
  56. package/src/signer/Signer.ts +0 -40
  57. package/src/signer/index.ts +0 -3
  58. package/src/storage/MemoryStorage.ts +0 -49
  59. package/src/storage/Storage.ts +0 -15
  60. package/src/storage/ZgStorage.ts +0 -124
  61. package/src/storage/index.ts +0 -3
  62. package/src/testing/index.ts +0 -1
  63. package/src/tools/adapters.ts +0 -177
  64. package/src/tools/helpers.ts +0 -199
  65. package/src/tools/index.ts +0 -59
  66. package/src/tools/types.ts +0 -47
  67. package/src/types.ts +0 -297
  68. package/src/verifier/index.ts +0 -1
  69. package/src/worker/Worker.ts +0 -98
  70. package/src/worker/index.ts +0 -1
  71. package/test/agent.test.ts +0 -193
  72. package/test/chain.test.ts +0 -145
  73. package/test/crypto.aes.test.ts +0 -81
  74. package/test/crypto.compat.test.ts +0 -33
  75. package/test/crypto.ecies.test.ts +0 -64
  76. package/test/envelope.test.ts +0 -82
  77. package/test/errors.test.ts +0 -64
  78. package/test/events.test.ts +0 -59
  79. package/test/fixtures/crypto.json +0 -16
  80. package/test/keystore.test.ts +0 -103
  81. package/test/network.test.ts +0 -53
  82. package/test/signer.test.ts +0 -66
  83. package/test/smoke.test.ts +0 -7
  84. package/test/storage.test.ts +0 -55
  85. package/test/worker.test.ts +0 -181
  86. package/tsconfig.build.json +0 -1
  87. package/tsconfig.json +0 -14
  88. package/tsup.config.ts +0 -32
  89. package/vitest.config.ts +0 -17
@@ -1,3 +1,4 @@
1
+ import { BlindMarket } from '../index.js';
1
2
  import type { A2ATaskState, AgentCapability, ExecutorProfile, Message } from '../types.js';
2
3
  export interface WorkerRuntimeConfig {
3
4
  apiKey: string;
@@ -13,6 +14,12 @@ export interface WorkerRuntimeConfig {
13
14
  browseIntervalMs?: number;
14
15
  watchIntervalMs?: number;
15
16
  maxConcurrentTasks?: number;
17
+ /**
18
+ * RPC URL used to sign + broadcast `submitEvidence` after `submitResult()`.
19
+ * Defaults to the 0G testnet RPC (matches `backend/agents/worker.js`'s
20
+ * default). Point this at the RPC for whichever chain your tasks settle on.
21
+ */
22
+ rpcUrl?: string;
16
23
  }
17
24
  export type ExecuteTaskHandler = (ctx: TaskContext) => Promise<Record<string, unknown>>;
18
25
  export interface TaskContext {
@@ -61,6 +68,10 @@ export type WorkerRuntimeEvent = {
61
68
  type: 'task_submitted';
62
69
  taskId: string;
63
70
  result: Record<string, unknown>;
71
+ } | {
72
+ type: 'task_finalized';
73
+ taskId: string;
74
+ finalize: Awaited<ReturnType<BlindMarket['finalize']>>;
64
75
  } | {
65
76
  type: 'task_failed';
66
77
  taskId: string;
@@ -108,6 +119,16 @@ export declare class WorkerRuntime {
108
119
  private browse;
109
120
  private watchForAssignment;
110
121
  private executeTask;
122
+ /**
123
+ * Decode a wrapped-key hex string — acceptTask()'s wrappedKey field is a
124
+ * single hex string (this executor's ECIES-wrapped AES key), not a
125
+ * Record. The old code here called `Object.values` on that STRING
126
+ * argument (treating it as if it were a Record), which returned its
127
+ * first CHARACTER (e.g. "0" from "04a1b2…") — `clean.length / 2` was
128
+ * then 0 and every decrypt got an empty key.
129
+ */
111
130
  private decodeWrappedKey;
112
131
  private hexToBytes;
132
+ /** Decode a base64 string (e.g. downloadBlob()'s `blob` field) into bytes. */
133
+ private base64ToBytes;
113
134
  }
@@ -1,3 +1,4 @@
1
+ import { ethers } from 'ethers';
1
2
  import { BlindMarket } from '../index.js';
2
3
  import { eciesDecrypt, aesDecrypt } from '../crypto/index.js';
3
4
  // ── Defaults ────────────────────────────────────────────────────────────────
@@ -5,6 +6,7 @@ const DEFAULTS = {
5
6
  browseIntervalMs: 15_000,
6
7
  watchIntervalMs: 5_000,
7
8
  maxConcurrentTasks: 3,
9
+ rpcUrl: 'https://evmrpc-testnet.0g.ai',
8
10
  };
9
11
  // ── WorkerRuntime ───────────────────────────────────────────────────────────
10
12
  export class WorkerRuntime {
@@ -184,39 +186,61 @@ export class WorkerRuntime {
184
186
  return;
185
187
  try {
186
188
  exec.status = 'assigned';
187
- // Accept task — get the ECIES-wrapped AES key
189
+ // Accept task — get the rootHash + this executor's ECIES-wrapped AES
190
+ // key. wrappedKey is a single hex string (this caller's slice), not a
191
+ // Record — see acceptTask()'s doc comment in ../index.ts.
188
192
  const acceptResult = await this.bb.acceptTask(taskId);
189
- exec.task = acceptResult.task;
193
+ exec.task = a2a;
190
194
  this.emit({ type: 'task_accepted', taskId });
191
- // Get full task detail for the storage root hash
192
- const detail = await this.bb.getTask(taskId);
193
- const taskHash = detail.taskHash;
194
- // Decrypt the wrapped key with the worker's private key
195
+ // Decrypt the brief. 'public' tasks carry no wrappedKey by design — the
196
+ // blob at rootHash is already plaintext, so skip ECIES/AES entirely.
197
+ // Download by acceptResult.rootHash (the 0G Storage pointer) — NOT the
198
+ // on-chain taskHash, which is a different value (sha256 of the
199
+ // ciphertext, used as the escrow's commitment) and isn't a valid
200
+ // storage lookup key.
195
201
  let instructions = '';
196
- if (acceptResult.wrappedKey && taskHash) {
197
- const wrappedBytes = this.decodeWrappedKey(acceptResult.wrappedKey);
198
- const aesKey = await eciesDecrypt(wrappedBytes, this.wallet.privateKey);
199
- // Download ciphertext from storage via REST API
200
- const storageResult = await this.bb.downloadBlob(taskHash);
201
- const ciphertext = this.hexToBytes(storageResult.data);
202
- // AES decrypt to get plaintext instructions
203
- const plaintext = await aesDecrypt(ciphertext, aesKey);
204
- instructions = new TextDecoder().decode(plaintext);
202
+ if (acceptResult.rootHash) {
203
+ const storageResult = await this.bb.downloadBlob(acceptResult.rootHash);
204
+ const blobBytes = this.base64ToBytes(storageResult.blob);
205
+ if (acceptResult.privacy === 'public') {
206
+ instructions = new TextDecoder().decode(blobBytes);
207
+ }
208
+ else if (acceptResult.wrappedKey) {
209
+ const wrappedBytes = this.decodeWrappedKey(acceptResult.wrappedKey);
210
+ const aesKey = await eciesDecrypt(wrappedBytes, this.wallet.privateKey);
211
+ const plaintext = await aesDecrypt(blobBytes, aesKey);
212
+ instructions = new TextDecoder().decode(plaintext);
213
+ }
205
214
  }
206
215
  exec.status = 'working';
207
216
  this.emit({ type: 'task_working', taskId });
208
217
  // Call the user-provided execution handler with decrypted instructions
209
218
  const result = await this.config.executeTask({
210
219
  taskId,
211
- task: acceptResult.task,
220
+ task: a2a,
212
221
  instructions,
213
222
  });
214
223
  exec.status = 'submitted';
215
224
  this.emit({ type: 'task_executed', taskId });
216
- // Submit result
225
+ // Submit result — the contract requires the assigned worker to sign
226
+ // submitEvidence personally (onlyWorker), so the backend hands back an
227
+ // unsigned tx instead of broadcasting it for us.
217
228
  const submitResult = await this.bb.submitResult(taskId, result);
229
+ // Sign + broadcast submitEvidence with the runtime's own wallet, wait
230
+ // for confirmation, then tell the backend to proceed with
231
+ // verification. Without this the on-chain task never leaves
232
+ // 'Assigned' and completeVerification always reverts — the task only
233
+ // LOOKS complete.
234
+ if (submitResult.unsignedSubmitEvidence) {
235
+ const provider = new ethers.JsonRpcProvider(this.config.rpcUrl);
236
+ const signer = new ethers.Wallet(this.wallet.privateKey, provider);
237
+ const tx = await signer.sendTransaction(submitResult.unsignedSubmitEvidence);
238
+ await tx.wait();
239
+ }
240
+ const finalizeResult = await this.bb.finalize(taskId);
218
241
  exec.status = 'completed';
219
- this.emit({ type: 'task_submitted', taskId, result: submitResult });
242
+ this.emit({ type: 'task_submitted', taskId, result });
243
+ this.emit({ type: 'task_finalized', taskId, finalize: finalizeResult });
220
244
  }
221
245
  catch (err) {
222
246
  exec.status = 'failed';
@@ -225,17 +249,18 @@ export class WorkerRuntime {
225
249
  }
226
250
  }
227
251
  // ── Helpers ─────────────────────────────────────────────────────────────
228
- decodeWrappedKey(wrappedKey) {
229
- // Backend returns wrapped key as hex or base64 in a record
230
- const hex = Object.values(wrappedKey)[0];
252
+ /**
253
+ * Decode a wrapped-key hex string — acceptTask()'s wrappedKey field is a
254
+ * single hex string (this executor's ECIES-wrapped AES key), not a
255
+ * Record. The old code here called `Object.values` on that STRING
256
+ * argument (treating it as if it were a Record), which returned its
257
+ * first CHARACTER (e.g. "0" from "04a1b2…") — `clean.length / 2` was
258
+ * then 0 and every decrypt got an empty key.
259
+ */
260
+ decodeWrappedKey(hex) {
231
261
  if (!hex)
232
262
  throw new Error('Empty wrapped key');
233
- const clean = hex.startsWith('0x') ? hex.slice(2) : hex;
234
- const bytes = new Uint8Array(clean.length / 2);
235
- for (let i = 0; i < bytes.length; i++) {
236
- bytes[i] = parseInt(clean.substring(i * 2, i * 2 + 2), 16);
237
- }
238
- return bytes;
263
+ return this.hexToBytes(hex);
239
264
  }
240
265
  hexToBytes(hex) {
241
266
  const clean = hex.startsWith('0x') ? hex.slice(2) : hex;
@@ -245,4 +270,8 @@ export class WorkerRuntime {
245
270
  }
246
271
  return bytes;
247
272
  }
273
+ /** Decode a base64 string (e.g. downloadBlob()'s `blob` field) into bytes. */
274
+ base64ToBytes(b64) {
275
+ return new Uint8Array(Buffer.from(b64, 'base64'));
276
+ }
248
277
  }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { ethers } from 'ethers';
2
- import type { Address, Hex, HealthStatus, PlatformStats, OpenTask, TaskDetail, CreateTaskTx, ExecutorProfile, RegisterExecutorInput, DeployedAgentInfo, AgentWalletInfo, ReputationInfo, LeaderboardEntry, StorageUploadResult, Message, AgentSearchResult, TaskTemplate, VerifyTaskInput, A2ATaskState, CreateAgentParams, CreateAgentResult } from './types.js';
2
+ import type { Address, Hex, RootHash, HealthStatus, PlatformStats, OpenTask, TaskDetail, CreateTaskTx, ExecutorProfile, RegisterExecutorInput, DeployedAgentInfo, AgentWalletInfo, ReputationInfo, LeaderboardEntry, StorageUploadResult, Message, AgentSearchResult, TaskTemplate, VerifyTaskInput, A2ATaskState, CreateAgentParams, CreateAgentResult } from './types.js';
3
3
  export interface BlindMarketConfig {
4
4
  /** Backend API base URL (default: https://api.blindmarket.xyz) */
5
5
  apiBase?: string;
@@ -207,15 +207,55 @@ export declare class BlindMarket {
207
207
  }>;
208
208
  /** Register intent to accept a task (bid). */
209
209
  bidOnTask(taskId: string): Promise<void>;
210
- /** Accept a task and get wrapped AES key. Requires executor auth. */
210
+ /**
211
+ * Accept a task and get the rootHash + ECIES-wrapped AES key for the
212
+ * caller's address. Requires executor auth.
213
+ *
214
+ * Note the shape here matches `backend/src/routes/a2a.ts`'s `/accept`
215
+ * handler exactly — there is no `task` field, and `wrappedKey` is a single
216
+ * hex string (this executor's slice), not a `Record`.
217
+ */
211
218
  acceptTask(taskId: string): Promise<{
212
- task: A2ATaskState;
213
- wrappedKey: Record<string, string>;
219
+ taskId: string;
220
+ status: string;
221
+ rootHash?: RootHash;
222
+ /** ECIES-wrapped AES key, hex-encoded with no 0x prefix. Absent on public tasks or legacy tasks with no brief. */
223
+ wrappedKey?: string;
224
+ /** 'public' means the blob at rootHash is plaintext — no wrappedKey by design. Absent means private/encrypted. */
225
+ privacy?: 'public' | 'private';
226
+ alreadySettled?: boolean;
227
+ assignTxHash?: Hex;
214
228
  }>;
215
- /** Submit result for an accepted task. */
229
+ /**
230
+ * Submit result for an accepted task. Returns an unsigned `submitEvidence`
231
+ * transaction — the contract requires the assigned worker to sign it
232
+ * personally (`onlyWorker`), so the caller must sign + broadcast
233
+ * `unsignedSubmitEvidence` and then call `finalize()`.
234
+ */
216
235
  submitResult(taskId: string, resultData: Record<string, unknown>): Promise<{
217
- unsignedTx: object;
218
- task: A2ATaskState;
236
+ taskId: string;
237
+ onChainTaskId?: string;
238
+ status: string;
239
+ evidenceHash?: Hex;
240
+ unsignedSubmitEvidence?: Record<string, unknown> | null;
241
+ }>;
242
+ /**
243
+ * Tell the backend your `submitEvidence` tx has confirmed on-chain so it
244
+ * can proceed with verification — auto-verify, or hand off to the poster
245
+ * / a designated verifier agent depending on the task's verificationMode.
246
+ * Call this after signing and broadcasting `unsignedSubmitEvidence` from
247
+ * `submitResult()`. Without this the escrow never settles.
248
+ */
249
+ finalize(taskId: string): Promise<{
250
+ taskId: string;
251
+ status: string;
252
+ verifier?: string;
253
+ awaitingPosterApproval?: boolean;
254
+ verificationResult?: {
255
+ passed: boolean;
256
+ reasons?: string[];
257
+ };
258
+ reconciled?: boolean;
219
259
  }>;
220
260
  /** Get tasks posted by the authenticated user. */
221
261
  getPostedTasks(): Promise<{
@@ -252,9 +292,15 @@ export declare class BlindMarket {
252
292
  getLeaderboard(limit?: number): Promise<LeaderboardEntry[]>;
253
293
  /** Upload an encrypted blob to 0G Storage. */
254
294
  uploadBlob(data: Hex): Promise<StorageUploadResult>;
255
- /** Download a blob by root hash. */
295
+ /**
296
+ * Download a blob by root hash. `blob` is base64-encoded raw bytes — the
297
+ * caller decodes (and, for encrypted tasks, decrypts) it client-side.
298
+ * Matches `backend/src/routes/storage.ts`'s `GET /:rootHash` handler,
299
+ * which returns `{ rootHash, blob }`, not `{ data }`.
300
+ */
256
301
  downloadBlob(rootHash: Hex): Promise<{
257
- data: Hex;
302
+ rootHash: Hex;
303
+ blob: string;
258
304
  }>;
259
305
  /** Send a message to another user or agent. */
260
306
  sendMessage(params: {
@@ -298,6 +344,14 @@ export declare class BlindMarket {
298
344
  * Poll an agent's status at a fixed interval. Returns an unsubscribe function.
299
345
  */
300
346
  watchAgent(agentId: string, callback: (agent: DeployedAgentInfo) => void, intervalMs?: number): () => void;
347
+ /**
348
+ * Canonical registration challenge signed by the agent wallet, proving
349
+ * control of `agentWallet`. Must stay byte-identical to
350
+ * `agentRegistrationMessage` in `backend/src/routes/registration.ts` — the
351
+ * SDK cannot import from the backend, so this is duplicated. A mismatch
352
+ * here silently breaks registration.
353
+ */
354
+ private static agentRegistrationMessage;
301
355
  /**
302
356
  * Start a device-flow registration session. Generates a magic-link URL
303
357
  * that the user opens in a browser to sign with their wallet.
@@ -305,6 +359,11 @@ export declare class BlindMarket {
305
359
  * After registering, call `BlindMarket.pollSession(token)` to wait for
306
360
  * the user to confirm and receive the API key.
307
361
  *
362
+ * Requires proof of control of `agentWallet`: pass either `agentSigner`
363
+ * (anything with a `signMessage`, e.g. an ethers `Wallet`) so the SDK signs
364
+ * the challenge itself, or a pre-computed `agentSignature` for callers
365
+ * holding a remote signer. Exactly one of the two is required.
366
+ *
308
367
  * @example
309
368
  * ```ts
310
369
  * const wallet = ethers.Wallet.createRandom();
@@ -312,6 +371,7 @@ export declare class BlindMarket {
312
371
  * agentName: 'my-agent',
313
372
  * agentWallet: wallet.address,
314
373
  * agentPublicKey: wallet.publicKey,
374
+ * agentSigner: wallet,
315
375
  * });
316
376
  * console.log('Open', url, 'to confirm');
317
377
  * const apiKey = await BlindMarket.pollSession(token);
@@ -322,6 +382,12 @@ export declare class BlindMarket {
322
382
  agentName: string;
323
383
  agentWallet: string;
324
384
  agentPublicKey: string;
385
+ /** Signs the registration challenge proving control of agentWallet. */
386
+ agentSigner?: {
387
+ signMessage(message: string): Promise<string>;
388
+ };
389
+ /** Pre-computed alternative to agentSigner, for callers holding a remote signer. */
390
+ agentSignature?: string;
325
391
  apiBase?: string;
326
392
  }): Promise<{
327
393
  token: string;
package/dist/index.js CHANGED
@@ -252,14 +252,36 @@ export class BlindMarket {
252
252
  async bidOnTask(taskId) {
253
253
  return this.req('POST', `/api/v1/a2a/tasks/${taskId}/bid`);
254
254
  }
255
- /** Accept a task and get wrapped AES key. Requires executor auth. */
255
+ /**
256
+ * Accept a task and get the rootHash + ECIES-wrapped AES key for the
257
+ * caller's address. Requires executor auth.
258
+ *
259
+ * Note the shape here matches `backend/src/routes/a2a.ts`'s `/accept`
260
+ * handler exactly — there is no `task` field, and `wrappedKey` is a single
261
+ * hex string (this executor's slice), not a `Record`.
262
+ */
256
263
  async acceptTask(taskId) {
257
264
  return this.req('POST', `/api/v1/a2a/tasks/${taskId}/accept`);
258
265
  }
259
- /** Submit result for an accepted task. */
266
+ /**
267
+ * Submit result for an accepted task. Returns an unsigned `submitEvidence`
268
+ * transaction — the contract requires the assigned worker to sign it
269
+ * personally (`onlyWorker`), so the caller must sign + broadcast
270
+ * `unsignedSubmitEvidence` and then call `finalize()`.
271
+ */
260
272
  async submitResult(taskId, resultData) {
261
273
  return this.req('POST', `/api/v1/a2a/tasks/${taskId}/submit`, { resultData });
262
274
  }
275
+ /**
276
+ * Tell the backend your `submitEvidence` tx has confirmed on-chain so it
277
+ * can proceed with verification — auto-verify, or hand off to the poster
278
+ * / a designated verifier agent depending on the task's verificationMode.
279
+ * Call this after signing and broadcasting `unsignedSubmitEvidence` from
280
+ * `submitResult()`. Without this the escrow never settles.
281
+ */
282
+ async finalize(taskId) {
283
+ return this.req('POST', `/api/v1/a2a/tasks/${taskId}/finalize`);
284
+ }
263
285
  /** Get tasks posted by the authenticated user. */
264
286
  async getPostedTasks() {
265
287
  return this.req('GET', '/api/v1/a2a/tasks/posted');
@@ -275,9 +297,9 @@ export class BlindMarket {
275
297
  */
276
298
  async verify(params) {
277
299
  return this.req('POST', '/api/v1/verification/verify', {
278
- taskId: params.taskId,
300
+ taskHash: params.taskHash,
279
301
  taskCategory: params.taskCategory,
280
- taskRequirements: params.taskRequirements,
302
+ ...(params.taskRequirements ? { taskRequirements: params.taskRequirements } : {}),
281
303
  evidenceSummary: params.evidenceSummary,
282
304
  });
283
305
  }
@@ -303,7 +325,12 @@ export class BlindMarket {
303
325
  async uploadBlob(data) {
304
326
  return this.req('POST', '/api/v1/storage/upload', { data });
305
327
  }
306
- /** Download a blob by root hash. */
328
+ /**
329
+ * Download a blob by root hash. `blob` is base64-encoded raw bytes — the
330
+ * caller decodes (and, for encrypted tasks, decrypts) it client-side.
331
+ * Matches `backend/src/routes/storage.ts`'s `GET /:rootHash` handler,
332
+ * which returns `{ rootHash, blob }`, not `{ data }`.
333
+ */
307
334
  async downloadBlob(rootHash) {
308
335
  return this.req('GET', `/api/v1/storage/${rootHash}`);
309
336
  }
@@ -391,6 +418,16 @@ export class BlindMarket {
391
418
  return () => clearInterval(id);
392
419
  }
393
420
  // ── Device-flow registration (static) ─────────────────────────────────────
421
+ /**
422
+ * Canonical registration challenge signed by the agent wallet, proving
423
+ * control of `agentWallet`. Must stay byte-identical to
424
+ * `agentRegistrationMessage` in `backend/src/routes/registration.ts` — the
425
+ * SDK cannot import from the backend, so this is duplicated. A mismatch
426
+ * here silently breaks registration.
427
+ */
428
+ static agentRegistrationMessage(agentName, agentWallet, agentPublicKey) {
429
+ return `BlindMarket agent registration\nname: ${agentName}\nwallet: ${agentWallet.toLowerCase()}\npubkey: ${agentPublicKey.toLowerCase()}`;
430
+ }
394
431
  /**
395
432
  * Start a device-flow registration session. Generates a magic-link URL
396
433
  * that the user opens in a browser to sign with their wallet.
@@ -398,6 +435,11 @@ export class BlindMarket {
398
435
  * After registering, call `BlindMarket.pollSession(token)` to wait for
399
436
  * the user to confirm and receive the API key.
400
437
  *
438
+ * Requires proof of control of `agentWallet`: pass either `agentSigner`
439
+ * (anything with a `signMessage`, e.g. an ethers `Wallet`) so the SDK signs
440
+ * the challenge itself, or a pre-computed `agentSignature` for callers
441
+ * holding a remote signer. Exactly one of the two is required.
442
+ *
401
443
  * @example
402
444
  * ```ts
403
445
  * const wallet = ethers.Wallet.createRandom();
@@ -405,6 +447,7 @@ export class BlindMarket {
405
447
  * agentName: 'my-agent',
406
448
  * agentWallet: wallet.address,
407
449
  * agentPublicKey: wallet.publicKey,
450
+ * agentSigner: wallet,
408
451
  * });
409
452
  * console.log('Open', url, 'to confirm');
410
453
  * const apiKey = await BlindMarket.pollSession(token);
@@ -412,6 +455,11 @@ export class BlindMarket {
412
455
  * ```
413
456
  */
414
457
  static async register(params) {
458
+ if (!params.agentSigner === !params.agentSignature) {
459
+ throw new ApiError(400, 'Exactly one of agentSigner or agentSignature is required to prove control of agentWallet');
460
+ }
461
+ const message = BlindMarket.agentRegistrationMessage(params.agentName, params.agentWallet, params.agentPublicKey);
462
+ const agentSignature = params.agentSignature ?? await params.agentSigner.signMessage(message);
415
463
  const base = params.apiBase ?? 'https://api.blindmarket.xyz';
416
464
  const res = await fetch(`${base}/api/v1/registration/session`, {
417
465
  method: 'POST',
@@ -420,6 +468,7 @@ export class BlindMarket {
420
468
  agentName: params.agentName,
421
469
  agentWallet: params.agentWallet,
422
470
  agentPublicKey: params.agentPublicKey,
471
+ agentSignature,
423
472
  }),
424
473
  });
425
474
  const json = await res.json();
@@ -124,13 +124,22 @@ export function createBlindMarketTools(bb) {
124
124
  return bb.updateAgent(a.agentId, a);
125
125
  }, ['agentId']),
126
126
  tool(bb, 'verify_task', 'Trigger AI/TEE verification for a submitted task result', {
127
- taskId: num('Numeric task ID'),
128
- taskCategory: str('Task category (e.g. photography, research)'),
129
- taskRequirements: str('What the task required'),
127
+ // taskHash, not a numeric id: ids collide across 0G and Base, so a number
128
+ // cannot identify a task. taskRequirements is optional and privileged —
129
+ // the backend builds the standard from the task the poster created and
130
+ // accepts this only from the poster or designated verifier.
131
+ taskHash: str('Task hash (bytes32 hex, 0x-prefixed)'),
132
+ taskCategory: str('Task category slug (letters, numbers, spaces, _ and - only)'),
133
+ taskRequirements: str('Optional supplemental requirements (poster/verifier only)'),
130
134
  evidenceSummary: str('Summary of submitted evidence'),
131
135
  }, async (a) => {
132
- return bb.verify(a);
133
- }, ['taskId', 'taskCategory', 'taskRequirements', 'evidenceSummary']),
136
+ return bb.verify({
137
+ taskHash: a.taskHash,
138
+ taskCategory: a.taskCategory,
139
+ ...(a.taskRequirements ? { taskRequirements: a.taskRequirements } : {}),
140
+ evidenceSummary: a.evidenceSummary,
141
+ });
142
+ }, ['taskHash', 'taskCategory', 'evidenceSummary']),
134
143
  tool(bb, 'get_reputation', "Get an address's on-chain + off-chain reputation", {
135
144
  address: str('Wallet address (0x...)'),
136
145
  }, async (a) => {
package/dist/types.d.ts CHANGED
@@ -228,9 +228,14 @@ export interface TaskTemplate {
228
228
  estimatedReward?: string;
229
229
  }
230
230
  export interface VerifyTaskInput {
231
- taskId: number;
231
+ /** taskHash (bytes32 hex). Not the numeric on-chain id — those collide
232
+ * across 0G and Base, so they cannot identify a task. */
233
+ taskHash: string;
232
234
  taskCategory: string;
233
- taskRequirements: string;
235
+ /** Optional supplemental requirements. The backend builds the standard being
236
+ * judged against from the task the poster created; this is accepted only
237
+ * from the poster or the designated verifier, never the executor. */
238
+ taskRequirements?: string;
234
239
  evidenceSummary: string;
235
240
  }
236
241
  /** Dot-notation access to valid capability strings. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blindmarket/sdk",
3
- "version": "0.1.8",
3
+ "version": "0.5.0",
4
4
  "description": "BlindMarket SDK — deploy agents, assign workers, verify evidence",
5
5
  "author": "BlindMarket Team",
6
6
  "license": "MIT",
@@ -31,6 +31,9 @@
31
31
  "vercel-ai-sdk"
32
32
  ],
33
33
  "type": "module",
34
+ "files": [
35
+ "dist"
36
+ ],
34
37
  "main": "./dist/index.js",
35
38
  "types": "./dist/index.d.ts",
36
39
  "exports": {
@@ -41,6 +44,10 @@
41
44
  "./tools": {
42
45
  "import": "./dist/tools/index.js",
43
46
  "types": "./dist/tools/index.d.ts"
47
+ },
48
+ "./crypto": {
49
+ "import": "./dist/crypto/index.js",
50
+ "types": "./dist/crypto/index.d.ts"
44
51
  }
45
52
  },
46
53
  "scripts": {
package/biome.json DELETED
@@ -1,19 +0,0 @@
1
- {
2
- "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
3
- "organizeImports": { "enabled": true },
4
- "linter": {
5
- "enabled": true,
6
- "rules": {
7
- "recommended": true,
8
- "suspicious": { "noExplicitAny": "error" },
9
- "style": { "useImportType": "error" }
10
- }
11
- },
12
- "formatter": {
13
- "enabled": true,
14
- "indentStyle": "space",
15
- "indentWidth": 2,
16
- "lineWidth": 120,
17
- "lineEnding": "lf"
18
- }
19
- }
@@ -1,121 +0,0 @@
1
- /**
2
- * Agent → Worker end-to-end demo, using MemoryStorage + a mock escrow so the
3
- * script runs anywhere without an RPC or funded wallet. Shows the full flow:
4
- *
5
- * 1. Agent creates an encrypted task (AES-encrypt instructions, upload blob)
6
- * 2. Agent assigns a worker (ECIES-wrap the AES key to worker's pubkey)
7
- * 3. Worker decrypts the instructions
8
- * 4. Worker submits evidence (seals a dual-recipient envelope for agent + TEE)
9
- * 5. Agent decrypts the evidence
10
- *
11
- * Run from sdk/: npx tsx examples/agent-demo.ts
12
- *
13
- * For the real chain, swap MockEscrow for `new ChainClient({ network, signer }).escrow`
14
- * and swap MemoryStorage for `new ZgStorage({ indexerUrl, rpcUrl, signer: ethersWallet })`.
15
- */
16
- import { Agent } from '../src/agent/index.js';
17
- import type { BlindEscrowClient, CreateTaskArgs } from '../src/chain/index.js';
18
- import { generateKeyPair } from '../src/crypto/index.js';
19
- import { EventBus, type SdkEvents } from '../src/events/index.js';
20
- import { InMemoryKeyStore } from '../src/keystore/index.js';
21
- import { PrivateKeySigner } from '../src/signer/index.js';
22
- import { MemoryStorage } from '../src/storage/index.js';
23
- import type { Address, Hex, RootHash, TaskId, TxReceiptLike } from '../src/types.js';
24
- import { Worker } from '../src/worker/index.js';
25
-
26
- // ── Mock escrow for offline demo (use real ChainClient in production) ──
27
- class MockEscrow {
28
- private nextId = 1n;
29
- private evidenceByTask = new Map<string, RootHash>();
30
- async createTask(args: CreateTaskArgs) {
31
- const taskId = this.nextId++;
32
- return { taskId, receipt: r('0xa') };
33
- }
34
- async assignWorker(_taskId: TaskId, _worker: Address) { return r('0xb'); }
35
- async cancelTask(_taskId: TaskId) { return r('0xc'); }
36
- async submitEvidence(taskId: TaskId, evidenceHash: Hex) {
37
- this.evidenceByTask.set(taskId.toString(), evidenceHash as RootHash);
38
- return r('0xd');
39
- }
40
- getEvidenceHash(taskId: TaskId) { return this.evidenceByTask.get(taskId.toString()); }
41
- }
42
- function r(prefix: string): TxReceiptLike {
43
- return { hash: (prefix + 'f'.repeat(63 - prefix.length + 2)).slice(0, 66) as Hex, blockNumber: 1, gasUsed: 21000n };
44
- }
45
-
46
- // ── Shared deps ──
47
- const AGENT_PK = '0x' + '1'.repeat(64);
48
- const WORKER_PK = '0x' + '2'.repeat(64);
49
- const USDC = '0x317227efcA18D004E12CA8046AEf7E1597458F25' as Address;
50
-
51
- const storage = new MemoryStorage();
52
- const escrow = new MockEscrow();
53
- const events = new EventBus<SdkEvents>();
54
- events.on('task.created', (p) => console.log(` • task.created taskId=${p.taskId} rootHash=${p.rootHash.slice(0, 18)}…`));
55
- events.on('task.assigned', (p) => console.log(` • task.assigned taskId=${p.taskId} worker=${p.worker.slice(0, 10)}…`));
56
- events.on('evidence.submitted', (p) => console.log(` • evidence.submitted taskId=${p.taskId} rootHash=${p.rootHash.slice(0, 18)}…`));
57
-
58
- // ── Identities ──
59
- const agentSigner = new PrivateKeySigner(AGENT_PK);
60
- const workerSigner = new PrivateKeySigner(WORKER_PK);
61
- const agentPubKey = await agentSigner.getPublicKey();
62
- const workerPubKey = await workerSigner.getPublicKey();
63
- const workerAddr = (await workerSigner.getAddress()) as Address;
64
- const enclaveKeys = generateKeyPair(); // TEE pubkey — fetched from broker in production
65
-
66
- const agent = new Agent({
67
- // biome-ignore lint/suspicious/noExplicitAny: demo mock
68
- escrow: escrow as any as BlindEscrowClient,
69
- storage,
70
- keystore: new InMemoryKeyStore(),
71
- signer: agentSigner,
72
- events,
73
- });
74
- const worker = new Worker({
75
- // biome-ignore lint/suspicious/noExplicitAny: demo mock
76
- escrow: escrow as any as BlindEscrowClient,
77
- // biome-ignore lint/suspicious/noExplicitAny: demo: no registry needed for this path
78
- registry: null as any,
79
- storage,
80
- signer: workerSigner,
81
- events,
82
- });
83
-
84
- // ── 1. Agent creates an encrypted task ──
85
- console.log('\n[1] Agent creates encrypted task');
86
- const created = await agent.createTask({
87
- instructions: 'Take 3 photos of the storefront at 123 Main St, Lagos',
88
- category: 'photography',
89
- locationZone: 'Lagos, NG',
90
- reward: { token: USDC, amount: 5_000000n },
91
- deadline: new Date(Date.now() + 24 * 3600_000),
92
- });
93
-
94
- // ── 2. Agent assigns the worker ──
95
- console.log('\n[2] Agent assigns worker (wraps AES key to worker pubkey)');
96
- const { wrappedKey } = await agent.assignWorker(created.taskId, workerAddr, workerPubKey);
97
-
98
- // ── 3. Worker decrypts the instructions ──
99
- console.log('\n[3] Worker decrypts instructions');
100
- const instructions = await worker.decryptInstructions({
101
- wrappedKey,
102
- workerPrivKey: WORKER_PK,
103
- taskHash: created.rootHash,
104
- });
105
- console.log(` → "${new TextDecoder().decode(instructions)}"`);
106
-
107
- // ── 4. Worker submits evidence ──
108
- console.log('\n[4] Worker submits evidence (dual-recipient envelope: agent + TEE)');
109
- const submitted = await worker.submitEvidence({
110
- taskId: created.taskId,
111
- evidence: 'photo1.jpg photo2.jpg photo3.jpg (attached)',
112
- agentPubKey,
113
- enclavePubKey: enclaveKeys.publicKey,
114
- });
115
-
116
- // ── 5. Agent fetches + decrypts evidence ──
117
- console.log('\n[5] Agent decrypts evidence');
118
- const recovered = await agent.fetchAndDecryptEvidence(submitted.rootHash, AGENT_PK);
119
- console.log(` → "${new TextDecoder().decode(recovered)}"`);
120
-
121
- console.log('\n✓ full lifecycle completed');