@blindmarket/sdk 0.1.8 → 0.4.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.
- package/dist/executor/WorkerRuntime.d.ts +21 -0
- package/dist/executor/WorkerRuntime.js +56 -27
- package/dist/index.d.ts +75 -9
- package/dist/index.js +52 -3
- package/package.json +8 -1
- package/biome.json +0 -19
- package/examples/agent-demo.ts +0 -121
- package/scripts/gen-crypto-fixtures.mjs +0 -72
- package/src/agent/Agent.ts +0 -122
- package/src/agent/index.ts +0 -1
- package/src/api/index.ts +0 -1
- package/src/chain/BlindEscrowClient.ts +0 -196
- package/src/chain/BlindReputationClient.ts +0 -60
- package/src/chain/ChainClient.ts +0 -65
- package/src/chain/IBlindMarketChain.ts +0 -109
- package/src/chain/TaskRegistryClient.ts +0 -91
- package/src/chain/abi/BlindEscrow.json +0 -1113
- package/src/chain/abi/BlindReputation.json +0 -447
- package/src/chain/abi/TaskRegistry.json +0 -586
- package/src/chain/abi/index.ts +0 -5
- package/src/chain/createChainAdapter.ts +0 -40
- package/src/chain/domain-types.ts +0 -92
- package/src/chain/errors.ts +0 -43
- package/src/chain/evm/EVMChainAdapter.ts +0 -212
- package/src/chain/evm/index.ts +0 -1
- package/src/chain/index.ts +0 -12
- package/src/compute/index.ts +0 -1
- package/src/crypto/aes.ts +0 -104
- package/src/crypto/constants.ts +0 -7
- package/src/crypto/ecies.ts +0 -60
- package/src/crypto/envelope.ts +0 -131
- package/src/crypto/hkdf.ts +0 -45
- package/src/crypto/index.ts +0 -6
- package/src/crypto/keys.ts +0 -56
- package/src/errors/BlindMarketError.ts +0 -38
- package/src/errors/codes.ts +0 -67
- package/src/errors/index.ts +0 -3
- package/src/errors/subclasses.ts +0 -59
- package/src/events/EventBus.ts +0 -45
- package/src/events/index.ts +0 -2
- package/src/events/types.ts +0 -13
- package/src/executor/WorkerRuntime.ts +0 -338
- package/src/executor/index.ts +0 -8
- package/src/index.ts +0 -636
- package/src/keystore/InMemoryKeyStore.ts +0 -146
- package/src/keystore/KeyStore.ts +0 -19
- package/src/keystore/index.ts +0 -2
- package/src/network/index.ts +0 -3
- package/src/network/presets.ts +0 -66
- package/src/network/resolve.ts +0 -31
- package/src/network/types.ts +0 -20
- package/src/signer/EthersSigner.ts +0 -82
- package/src/signer/PrivateKeySigner.ts +0 -19
- package/src/signer/Signer.ts +0 -40
- package/src/signer/index.ts +0 -3
- package/src/storage/MemoryStorage.ts +0 -49
- package/src/storage/Storage.ts +0 -15
- package/src/storage/ZgStorage.ts +0 -124
- package/src/storage/index.ts +0 -3
- package/src/testing/index.ts +0 -1
- package/src/tools/adapters.ts +0 -177
- package/src/tools/helpers.ts +0 -199
- package/src/tools/index.ts +0 -59
- package/src/tools/types.ts +0 -47
- package/src/types.ts +0 -297
- package/src/verifier/index.ts +0 -1
- package/src/worker/Worker.ts +0 -98
- package/src/worker/index.ts +0 -1
- package/test/agent.test.ts +0 -193
- package/test/chain.test.ts +0 -145
- package/test/crypto.aes.test.ts +0 -81
- package/test/crypto.compat.test.ts +0 -33
- package/test/crypto.ecies.test.ts +0 -64
- package/test/envelope.test.ts +0 -82
- package/test/errors.test.ts +0 -64
- package/test/events.test.ts +0 -59
- package/test/fixtures/crypto.json +0 -16
- package/test/keystore.test.ts +0 -103
- package/test/network.test.ts +0 -53
- package/test/signer.test.ts +0 -66
- package/test/smoke.test.ts +0 -7
- package/test/storage.test.ts +0 -55
- package/test/worker.test.ts +0 -181
- package/tsconfig.build.json +0 -1
- package/tsconfig.json +0 -14
- package/tsup.config.ts +0 -32
- 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
|
|
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 =
|
|
193
|
+
exec.task = a2a;
|
|
190
194
|
this.emit({ type: 'task_accepted', taskId });
|
|
191
|
-
//
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
//
|
|
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.
|
|
197
|
-
const
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
213
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
218
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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');
|
|
@@ -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
|
-
/**
|
|
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();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blindmarket/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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
|
-
}
|
package/examples/agent-demo.ts
DELETED
|
@@ -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');
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Generates cross-compat crypto fixtures using Node's built-in crypto, matching
|
|
4
|
-
* backend/src/services/crypto.ts byte-for-byte. The SDK's test/crypto.compat.test.ts
|
|
5
|
-
* loads these and verifies WebCrypto-based decryption produces the same plaintext.
|
|
6
|
-
*
|
|
7
|
-
* Run: node scripts/gen-crypto-fixtures.mjs
|
|
8
|
-
* Output: test/fixtures/crypto.json (committed to the repo; regenerate only on
|
|
9
|
-
* intentional wire-format changes).
|
|
10
|
-
*/
|
|
11
|
-
import { createCipheriv, createECDH, hkdfSync, randomBytes } from 'node:crypto';
|
|
12
|
-
import { writeFileSync } from 'node:fs';
|
|
13
|
-
import { dirname, resolve } from 'node:path';
|
|
14
|
-
import { fileURLToPath } from 'node:url';
|
|
15
|
-
|
|
16
|
-
const IV_LENGTH = 12;
|
|
17
|
-
const TAG_LENGTH = 16;
|
|
18
|
-
const KEY_LENGTH = 32;
|
|
19
|
-
const ECIES_HKDF_INFO = 'BlindMarket-ECIES-v1';
|
|
20
|
-
|
|
21
|
-
function aesEncrypt(plaintext, key) {
|
|
22
|
-
const iv = randomBytes(IV_LENGTH);
|
|
23
|
-
const cipher = createCipheriv('aes-256-gcm', key, iv, { authTagLength: TAG_LENGTH });
|
|
24
|
-
const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
25
|
-
const tag = cipher.getAuthTag();
|
|
26
|
-
// [IV][tag][ciphertext] — matches backend/src/services/crypto.ts
|
|
27
|
-
return Buffer.concat([iv, tag, encrypted]);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function eciesEncrypt(plaintext, recipientPubKeyBuf) {
|
|
31
|
-
const ephemeral = createECDH('secp256k1');
|
|
32
|
-
ephemeral.generateKeys();
|
|
33
|
-
const shared = ephemeral.computeSecret(recipientPubKeyBuf);
|
|
34
|
-
const aesKey = Buffer.from(hkdfSync('sha256', shared, '', ECIES_HKDF_INFO, KEY_LENGTH));
|
|
35
|
-
const aesBlob = aesEncrypt(plaintext, aesKey);
|
|
36
|
-
return Buffer.concat([ephemeral.getPublicKey(), aesBlob]);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const recipient = createECDH('secp256k1');
|
|
40
|
-
recipient.generateKeys();
|
|
41
|
-
const privateKey = recipient.getPrivateKey('hex');
|
|
42
|
-
const publicKey = recipient.getPublicKey('hex', 'uncompressed');
|
|
43
|
-
|
|
44
|
-
const plaintext = 'hello cross-compat';
|
|
45
|
-
const ptBuf = Buffer.from(plaintext, 'utf-8');
|
|
46
|
-
|
|
47
|
-
// AES fixture
|
|
48
|
-
const aesKey = randomBytes(KEY_LENGTH);
|
|
49
|
-
const aesBlob = aesEncrypt(ptBuf, aesKey);
|
|
50
|
-
|
|
51
|
-
// ECIES fixture (recipient's pubkey is encoded as Buffer for computeSecret)
|
|
52
|
-
const eciesBlob = eciesEncrypt(ptBuf, Buffer.from(publicKey, 'hex'));
|
|
53
|
-
|
|
54
|
-
const fixture = {
|
|
55
|
-
_note:
|
|
56
|
-
'Generated by scripts/gen-crypto-fixtures.mjs using Node built-in crypto, matching backend/src/services/crypto.ts. If the SDK decrypts these back to the expected plaintext, wire-format compatibility is proven. Regenerate only on intentional format changes.',
|
|
57
|
-
hkdf_info: ECIES_HKDF_INFO,
|
|
58
|
-
keypair: { privateKey, publicKey },
|
|
59
|
-
plaintext,
|
|
60
|
-
aes: {
|
|
61
|
-
key: aesKey.toString('hex'),
|
|
62
|
-
blob: aesBlob.toString('hex'),
|
|
63
|
-
},
|
|
64
|
-
ecies: {
|
|
65
|
-
blob: eciesBlob.toString('hex'),
|
|
66
|
-
},
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
70
|
-
const out = resolve(__dirname, '..', 'test', 'fixtures', 'crypto.json');
|
|
71
|
-
writeFileSync(out, `${JSON.stringify(fixture, null, 2)}\n`);
|
|
72
|
-
console.log(`wrote ${out}`);
|