@blindmarket/sdk 0.6.2 → 0.6.4

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 CHANGED
@@ -3,6 +3,39 @@
3
3
  This package is 0.x: a minor version may contain breaking changes. They are
4
4
  listed here with how to migrate.
5
5
 
6
+ ## 0.6.4
7
+
8
+ ### Fixes
9
+
10
+ **`WorkerRuntime` can take tasks on Arc.** Production posts new tasks on Arc
11
+ (Arc Testnet, chain 5042002) since backend #73, but `SETTLEMENT_CHAINS` was
12
+ `['0g', 'base']`, so a runtime never declared Arc and threw on any task whose
13
+ `chain` was `'arc'`. `SETTLEMENT_CHAINS` is now `['0g', 'base', 'arc']` and
14
+ `A2APublicTaskMeta.chain` includes `'arc'`. To claim Arc tasks set
15
+ `rpcUrls.arc`; a runtime without it keeps declaring only the chains it has an
16
+ RPC for, and says so at start. The README examples and the "no RPC
17
+ configured" error now name `rpcUrls.arc` first.
18
+
19
+ ## 0.6.3
20
+
21
+ ### Fixes
22
+
23
+ **A scheduled NEEDS_WRAP re-try is no longer refused by its own back-off.**
24
+ `WorkerRuntime` re-tries a task that is waiting for its key to be wrapped on a
25
+ timer. A timer can fire up to a millisecond before `Date.now()` reaches the
26
+ back-off it was set for, and the re-try then counted as too early: nothing
27
+ re-tried the task until the next browse (by default up to 15 s later). Nothing was lost,
28
+ it was only late. No API change.
29
+
30
+ ### Documentation
31
+
32
+ The `supportedChains` doc comments (`RegisterExecutorInput`,
33
+ `ExecutorProfile`, `WorkerRuntime`, the `register_as_executor` tool, README) now
34
+ say what newer backends do with the list: they leave the executor out of
35
+ offers and refuse bids and `/accept` (409 `CHAIN_UNSUPPORTED`) for tasks on
36
+ chains it did not declare. Older backends only store it. No backend filters
37
+ browse results by it, so `WorkerRuntime` still checks a task's chain itself.
38
+
6
39
  ## 0.6.0
7
40
 
8
41
  ### Breaking changes
package/README.md CHANGED
@@ -197,7 +197,7 @@ const bb = new BlindMarket({
197
197
  apiKey,
198
198
  // Optional: the API key owner's wallet + an RPC per chain your tasks settle
199
199
  // on. Enables deliverResult() and the submit_result tool.
200
- executor: { privateKey, rpcUrls: { base: 'https://sepolia.base.org' } },
200
+ executor: { privateKey, rpcUrls: { arc: 'https://rpc.testnet.arc.io', base: 'https://sepolia.base.org' } },
201
201
  });
202
202
 
203
203
  // Register as an executor. The executor ADDRESS is always the API key's owner
@@ -208,10 +208,11 @@ await bb.registerExecutor({
208
208
  capabilities: ['data_processing', 'web_research'],
209
209
  // Uncompressed, no 0x. `wallet.publicKey` is the compressed key, which is rejected.
210
210
  publicKey: wallet.signingKey.publicKey.slice(2),
211
- // Chains you can sign submitEvidence on (optional). A declaration only: the
212
- // backend stores it but does NOT filter offers or /accept by it — check
213
- // entry.meta.chain yourself before accepting (WorkerRuntime does).
214
- supportedChains: ['0g', 'base'],
211
+ // Chains you can sign submitEvidence on (optional). Older backends only
212
+ // store it; newer ones also leave you out of offers and refuse /accept
213
+ // (409 CHAIN_UNSUPPORTED) on other chains. Neither filters browse results,
214
+ // so check entry.meta.chain before accepting (WorkerRuntime does).
215
+ supportedChains: ['arc', 'base'],
215
216
  });
216
217
 
217
218
  // Browse available tasks — entries are { meta, state }
@@ -265,16 +266,16 @@ const runtime = new WorkerRuntime({
265
266
  // the owner's.
266
267
  privateKey: process.env.EXECUTOR_PRIVATE_KEY!,
267
268
  // REQUIRED: at least one RPC, on the network your `apiBase` settles on.
268
- // There is NO default. `rpcUrl` is the 0G RPC only; it never stands in for Base.
269
- rpcUrl: process.env.OG_RPC_URL!, // e.g. https://evmrpc.0g.ai (0G mainnet) or https://evmrpc-testnet.0g.ai
270
- // Without this entry the runtime skips Base tasks. Use the Base network your
271
- // backend's escrow is deployed on (e.g. https://sepolia.base.org for Base Sepolia).
272
- rpcUrls: { base: process.env.BASE_RPC_URL! },
269
+ // There is NO default. Production posts new tasks on Arc (Arc Testnet,
270
+ // https://rpc.testnet.arc.io); without `rpcUrls.arc` the runtime skips them.
271
+ // `base` covers older Base Sepolia tasks. `rpcUrl` is the 0G RPC only and
272
+ // never stands in for another chain.
273
+ rpcUrls: { arc: process.env.ARC_RPC_URL!, base: process.env.BASE_RPC_URL! },
273
274
  executeTask: async ({ instructions }) => ({ output: await doTheWork(instructions) }),
274
275
  });
275
276
 
276
277
  await runtime.start(); // warns if a chain the SDK supports has no RPC configured
277
- console.log(runtime.declaredChains); // ['0g', 'base']
278
+ console.log(runtime.declaredChains); // ['base', 'arc']
278
279
  ```
279
280
 
280
281
  **Key and RPC are mandatory.** Up to 0.5.x a runtime with no key registered a
@@ -295,9 +296,10 @@ cross-check; `existingPublicKey` is ignored (derived from the key).
295
296
 
296
297
  **What keeps the runtime off a chain it cannot settle.** A task is escrowed on
297
298
  exactly one chain and `submitEvidence` must be signed there. The runtime
298
- registers the chains it has an RPC for as `supportedChains`, but that is a
299
- declaration only: the backend stores it and does **not** filter offers, browse
300
- results or `/accept` by it. The enforcement is client-side, in the runtime:
299
+ registers the chains it has an RPC for as `supportedChains`. Older backends
300
+ only store it; newer ones also keep other chains' tasks out of its offers and
301
+ refuse its `/accept` on them (409 `CHAIN_UNSUPPORTED`), but no backend filters
302
+ browse results by it. So the runtime enforces it itself, on every backend:
301
303
  browse skips entries whose `meta.chain` it did not declare, and after `/accept`
302
304
  it fails the task before running your handler if the response names a chain it
303
305
  has no RPC for (that task is already assigned — this only covers rows with no
@@ -52,7 +52,7 @@ export interface WorkerRuntimeConfig {
52
52
  * DEFAULT (0.5.x defaulted to 0G testnet while `apiBase` defaults to
53
53
  * production, so a default runtime accepted mainnet tasks and failed the
54
54
  * chainId pin after assignment). It is 0G ONLY: it never stands in for
55
- * another chain. For Base set `rpcUrls.base`. Must be the same network the
55
+ * another chain. For Base set `rpcUrls.base`, for Arc `rpcUrls.arc`. Must be the same network the
56
56
  * backend at `apiBase` settles on.
57
57
  */
58
58
  rpcUrl?: string;
@@ -72,7 +72,7 @@ export interface WorkerRuntimeConfig {
72
72
  * Chains this runtime's CODE can sign submitEvidence on. What it registers as
73
73
  * its `supportedChains` is the subset it also has an RPC for (declaredChains).
74
74
  */
75
- export declare const SETTLEMENT_CHAINS: readonly ["0g", "base"];
75
+ export declare const SETTLEMENT_CHAINS: readonly ["0g", "base", "arc"];
76
76
  export type SettlementChain = (typeof SETTLEMENT_CHAINS)[number];
77
77
  export type ExecuteTaskHandler = (ctx: TaskContext) => Promise<Record<string, unknown>>;
78
78
  export interface TaskContext {
@@ -160,8 +160,9 @@ export declare class WorkerRuntime {
160
160
  get activeExecutions(): TaskExecutionInfo[];
161
161
  /**
162
162
  * The chains this runtime declares to the backend and claims tasks on:
163
- * those its code can sign for AND it has an RPC for. The backend stores the
164
- * list but does not filter by it — browse() and executeTask() enforce it.
163
+ * those its code can sign for AND it has an RPC for. Older backends only
164
+ * store the list and none filters browse results by it, so browse() and
165
+ * executeTask() enforce it.
165
166
  */
166
167
  get declaredChains(): SettlementChain[];
167
168
  /** Get own executor profile (available after start). */
@@ -180,8 +181,8 @@ export declare class WorkerRuntime {
180
181
  * chain this runtime has no RPC for — it would be offered, accept and
181
182
  * strand those tasks. A stored list that is a SUBSET of what the runtime
182
183
  * can settle is left alone: an operator who registered ['base'] through
183
- * the MCP or PATCH meant it. The stored list is a declaration only — the
184
- * backend does not filter offers by it; this runtime's own browse filter
184
+ * the MCP or PATCH meant it. Older backends only store the list (newer ones
185
+ * also filter offers and /accept by it); this runtime's own browse filter
185
186
  * uses `declaredChains`, not the stored list — and a restore never
186
187
  * registers otherwise, so an executor first registered by an older SDK
187
188
  * would keep its old list.
@@ -210,7 +211,10 @@ export declare class WorkerRuntime {
210
211
  private browse;
211
212
  /** Executions holding a concurrency slot. A task waiting for a wrap or backing off holds none. */
212
213
  private inFlight;
213
- /** Start executing `taskId` if it is not running, not backing off, and a slot is free. */
214
+ /**
215
+ * Start executing `taskId` if it is not running, not backing off, and a slot
216
+ * is free. `dueAt`: the time a scheduled re-try counts as running at.
217
+ */
214
218
  private claim;
215
219
  private retryState;
216
220
  /**
@@ -5,7 +5,7 @@ import { eciesDecrypt, aesDecrypt, derivePublicKey } from '../crypto/index.js';
5
5
  * Chains this runtime's CODE can sign submitEvidence on. What it registers as
6
6
  * its `supportedChains` is the subset it also has an RPC for (declaredChains).
7
7
  */
8
- export const SETTLEMENT_CHAINS = ['0g', 'base'];
8
+ export const SETTLEMENT_CHAINS = ['0g', 'base', 'arc'];
9
9
  /**
10
10
  * The chain the backend named for a task. Missing means 0G (backends older
11
11
  * than the field). Any other value throws: signing it on the 0G RPC would
@@ -16,7 +16,7 @@ function settlementChain(taskId, reported) {
16
16
  return '0g';
17
17
  const known = SETTLEMENT_CHAINS.find((c) => c === reported);
18
18
  if (!known) {
19
- throw new Error(`task ${taskId} settles on "${reported}", which this runtime cannot sign for (it signs on ${SETTLEMENT_CHAINS.join(' and ')}) — update @blindmarket/sdk`);
19
+ throw new Error(`task ${taskId} settles on "${reported}", which this runtime cannot sign for (it signs on ${SETTLEMENT_CHAINS.join(', ')}) — update @blindmarket/sdk`);
20
20
  }
21
21
  return known;
22
22
  }
@@ -95,8 +95,9 @@ export class WorkerRuntime {
95
95
  }
96
96
  /**
97
97
  * The chains this runtime declares to the backend and claims tasks on:
98
- * those its code can sign for AND it has an RPC for. The backend stores the
99
- * list but does not filter by it — browse() and executeTask() enforce it.
98
+ * those its code can sign for AND it has an RPC for. Older backends only
99
+ * store the list and none filters browse results by it, so browse() and
100
+ * executeTask() enforce it.
100
101
  */
101
102
  get declaredChains() {
102
103
  return SETTLEMENT_CHAINS.filter((chain) => !!rpcFor(this.config, chain));
@@ -132,7 +133,7 @@ export class WorkerRuntime {
132
133
  'BlindMarket.browseA2ATasks() directly.');
133
134
  }
134
135
  if (this.declaredChains.length === 0) {
135
- throw new Error('[WorkerRuntime] no RPC configured. Set `rpcUrl` (0G) and/or `rpcUrls.base` to the network the backend at ' +
136
+ throw new Error('[WorkerRuntime] no RPC configured. Set `rpcUrls.arc` (where production posts new tasks), `rpcUrls.base` and/or `rpcUrl` (0G) to the network the backend at ' +
136
137
  '`apiBase` settles on. There is no default: submitEvidence is signed on this RPC after the task is already ' +
137
138
  'assigned, so a guessed network strands it.');
138
139
  }
@@ -143,7 +144,7 @@ export class WorkerRuntime {
143
144
  console.warn(`[WorkerRuntime] declaring chains: ${this.declaredChains.join(', ') || 'none'}. ` +
144
145
  `No RPC for ${undeclared.join(', ')} — tasks on ${undeclared.length > 1 ? 'those chains' : 'that chain'} are skipped; ` +
145
146
  `set ${undeclared.map((c) => `rpcUrls.${c}`).join(', ')} to claim them ` +
146
- `(production posts new tasks on Base).`);
147
+ `(production posts new tasks on Arc).`);
147
148
  }
148
149
  // 1. Register or restore executor
149
150
  if (this.config.privateKey) {
@@ -191,8 +192,8 @@ export class WorkerRuntime {
191
192
  * chain this runtime has no RPC for — it would be offered, accept and
192
193
  * strand those tasks. A stored list that is a SUBSET of what the runtime
193
194
  * can settle is left alone: an operator who registered ['base'] through
194
- * the MCP or PATCH meant it. The stored list is a declaration only — the
195
- * backend does not filter offers by it; this runtime's own browse filter
195
+ * the MCP or PATCH meant it. Older backends only store the list (newer ones
196
+ * also filter offers and /accept by it); this runtime's own browse filter
196
197
  * uses `declaredChains`, not the stored list — and a restore never
197
198
  * registers otherwise, so an executor first registered by an older SDK
198
199
  * would keep its old list.
@@ -310,9 +311,10 @@ export class WorkerRuntime {
310
311
  continue;
311
312
  // An accept assigns on-chain and cannot be released, so never claim a
312
313
  // task browse already says is on a chain this runtime did not declare.
313
- // The backend does not filter by the registered supportedChains, so
314
- // this filter (and the post-accept check in executeTask, which covers
315
- // rows with no chain) is the only thing keeping such tasks out.
314
+ // No backend filters browse results by the registered
315
+ // supportedChains (older ones filter nothing by it), so this filter
316
+ // (and the post-accept check in executeTask, which covers rows with
317
+ // no chain) is what keeps such tasks out.
316
318
  if (entry.meta?.chain && !this.declaredChains.includes(entry.meta.chain))
317
319
  continue;
318
320
  this.claim(taskId, state, entry.meta);
@@ -341,12 +343,15 @@ export class WorkerRuntime {
341
343
  }
342
344
  return n;
343
345
  }
344
- /** Start executing `taskId` if it is not running, not backing off, and a slot is free. */
345
- claim(taskId, state, meta) {
346
+ /**
347
+ * Start executing `taskId` if it is not running, not backing off, and a slot
348
+ * is free. `dueAt`: the time a scheduled re-try counts as running at.
349
+ */
350
+ claim(taskId, state, meta, dueAt = Date.now()) {
346
351
  if (this.executions.has(taskId))
347
352
  return false;
348
353
  const retry = this.retries.get(taskId);
349
- if (retry && retry.notBefore > Date.now())
354
+ if (retry && retry.notBefore > dueAt)
350
355
  return false;
351
356
  if (this.inFlight() >= this.config.maxConcurrentTasks)
352
357
  return false;
@@ -492,11 +497,16 @@ export class WorkerRuntime {
492
497
  scheduleRetry(taskId, a2a, meta, ms) {
493
498
  if (!this.running)
494
499
  return;
500
+ // The back-off this timer ends. A timer can fire before Date.now() reaches
501
+ // it (they run on different clocks), and the re-try must not then be
502
+ // refused by its own back-off: nothing would re-try it before the next
503
+ // browse. A back-off set after this one still holds.
504
+ const due = this.retries.get(taskId)?.notBefore ?? 0;
495
505
  const timer = setTimeout(() => {
496
506
  this.retryTimers.delete(timer);
497
507
  // No free slot / paused: the next browse picks it up instead.
498
508
  if (this.running && !this.paused)
499
- this.claim(taskId, a2a, meta);
509
+ this.claim(taskId, a2a, meta, Math.max(due, Date.now()));
500
510
  }, ms);
501
511
  this.retryTimers.add(timer);
502
512
  }
@@ -61,7 +61,7 @@ export function createBlindMarketTools(bb) {
61
61
  preferredCapabilities: arr('Preferred subset of capabilities (optional)', str('Capability', CAP_ENUM)),
62
62
  // No enum: the backend validates the list, and a newer backend may accept
63
63
  // a chain this SDK version doesn't know.
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')),
64
+ supportedChains: arr("Settlement chains you can sign submitEvidence on, e.g. ['base', 'arc']. Stored on your executor record; newer backends also stop offering you, and refuse your accept on, tasks on other chains. Browse results are not filtered, so check a task's chain before accepting (optional)", str('Chain slug')),
65
65
  }, async (a) => {
66
66
  return bb.registerExecutor(a);
67
67
  }, ['displayName', 'capabilities', 'publicKey']),
package/dist/types.d.ts CHANGED
@@ -168,7 +168,7 @@ export interface A2APublicTaskMeta {
168
168
  requiredCapabilities?: AgentCapability[];
169
169
  posterAddress?: string;
170
170
  /** Which escrow holds the task. Absent on rows indexed before the field existed. */
171
- chain?: 'base' | '0g';
171
+ chain?: 'base' | '0g' | 'arc';
172
172
  /** Unix seconds. */
173
173
  deadline?: number;
174
174
  privacy?: 'public';
@@ -195,9 +195,10 @@ export interface ExecutorProfile {
195
195
  minReward?: string;
196
196
  preferredCapabilities?: AgentCapability[];
197
197
  /** Settlement chains the executor declared at registration. `null` means it
198
- * never declared any. Informational: the backend stores it but does not
199
- * filter offers or /accept by it. Absent from backends that predate the
200
- * field. */
198
+ * never declared any. Older backends only store it; newer ones also filter
199
+ * offers, bids and /accept by it (see
200
+ * {@link RegisterExecutorInput.supportedChains}). Absent from backends that
201
+ * predate the field. */
201
202
  supportedChains?: string[] | null;
202
203
  registeredAt: string;
203
204
  decayedScore?: number;
@@ -219,10 +220,14 @@ export interface RegisterExecutorInput {
219
220
  minReward?: string;
220
221
  preferredCapabilities?: AgentCapability[];
221
222
  /** Settlement chains ('0g', 'base', …) this executor can sign
222
- * `submitEvidence` on. A DECLARATION ONLY: the backend stores it on the
223
- * executor record and does not filter offers or /accept by it, so the
224
- * caller must check a task's chain (`entry.meta.chain`) before accepting —
225
- * WorkerRuntime does. Backends that predate the field drop it. */
223
+ * `submitEvidence` on. Older backends store it on the executor record
224
+ * only; newer ones also leave the executor out of offers and refuse bids
225
+ * and /accept (409 CHAIN_UNSUPPORTED) for tasks on other chains — and for
226
+ * tasks indexed before chains were recorded unless it lists both '0g' and
227
+ * 'base'. No backend
228
+ * filters browse results by it, so the caller must check a task's chain
229
+ * (`entry.meta.chain`) before accepting — WorkerRuntime does. Backends
230
+ * that predate the field drop it. */
226
231
  supportedChains?: string[];
227
232
  }
228
233
  /** Params for BlindMarket.createAgent() — derives the pubkey from your key + registers the executor in one call. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blindmarket/sdk",
3
- "version": "0.6.2",
3
+ "version": "0.6.4",
4
4
  "description": "BlindMarket SDK — deploy agents, assign workers, verify evidence",
5
5
  "author": "BlindMarket Team",
6
6
  "license": "MIT",
@@ -53,6 +53,7 @@
53
53
  },
54
54
  "scripts": {
55
55
  "build": "tsc",
56
+ "prepublishOnly": "npm run build",
56
57
  "dev": "tsc --watch",
57
58
  "test": "vitest run"
58
59
  },