@blindmarket/sdk 0.7.0 → 0.9.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/CHANGELOG.md +130 -0
- package/README.md +101 -8
- package/dist/chain/abi/BlindEscrow.json +154 -0
- package/dist/escrowCalls.d.ts +51 -0
- package/dist/escrowCalls.js +97 -0
- package/dist/executor/WorkerRuntime.d.ts +32 -0
- package/dist/executor/WorkerRuntime.js +120 -0
- package/dist/index.d.ts +354 -5
- package/dist/index.js +821 -159
- package/dist/network/presets.d.ts +1 -1
- package/dist/network/presets.js +1 -1
- package/dist/onchain.d.ts +9 -5
- package/dist/onchain.js +31 -6
- package/dist/posting.d.ts +63 -0
- package/dist/posting.js +168 -0
- package/dist/settlementPins.d.ts +20 -0
- package/dist/settlementPins.js +11 -0
- package/dist/tools/helpers.js +1 -1
- package/dist/types.d.ts +13 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,136 @@
|
|
|
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.9.0
|
|
7
|
+
|
|
8
|
+
### New
|
|
9
|
+
|
|
10
|
+
- **`postTasks(rows, opts)` posts many tasks at once** (docs/BULK-POSTING.md).
|
|
11
|
+
Every row is checked and its brief sealed before anything is uploaded or
|
|
12
|
+
sent. A row the escrow or the backend would refuse throws 400
|
|
13
|
+
`INVALID_ROWS`, listing each one in `err.body.errors` (`{ index, code,
|
|
14
|
+
message }`), with nothing sent. Rows that are the same public brief count
|
|
15
|
+
as refused (`DUPLICATE_BRIEF`). The wallet must hold the total, and the
|
|
16
|
+
escrow is approved for it once, just before the first funding transaction,
|
|
17
|
+
instead of once per task.
|
|
18
|
+
- **Escrow with `createTasks`:** `GET /health/settlement` reports
|
|
19
|
+
`batchCreate.supported` for the chain. Up to `chunkSize` rows (default
|
|
20
|
+
20, at most the escrow's `maxBatch`) then share one transaction and one
|
|
21
|
+
listing call. The transaction's gas limit is estimated locally with 20%
|
|
22
|
+
headroom.
|
|
23
|
+
- **Otherwise:** each row is its own `createTask`, as `postTask()` sends it.
|
|
24
|
+
- **Safety:** every transaction is checked before signing, as `postTask()`
|
|
25
|
+
checks one. A `createTasks` must hold exactly these tasks, in this order,
|
|
26
|
+
for this token, escrow and chain.
|
|
27
|
+
- **Where the run stops:** a row the backend refuses before funding fails
|
|
28
|
+
alone and the run goes on. A funding that reverts or cannot be confirmed,
|
|
29
|
+
a listing that fails, or a backend that builds the wrong transaction or
|
|
30
|
+
stays unreachable stops the run there (`result.stopped`).
|
|
31
|
+
- **No double funding:** a funded row that is not listed comes back
|
|
32
|
+
`'unlisted'` with its `indexParams`, and nothing is funded twice.
|
|
33
|
+
- **Brief storage:** briefs are stored two per `upload-batch` request, one
|
|
34
|
+
request after another (0G stores a brief in 20–40 s, and production's
|
|
35
|
+
edge gives up at ~100 s).
|
|
36
|
+
- A pair that fails transiently is re-sent one brief per request, in
|
|
37
|
+
order, each with the backoff; a stored brief returns at once. Transient
|
|
38
|
+
means: a dropped connection, this client's own timeout, 429, 502, 503,
|
|
39
|
+
504, or Cloudflare's non-JSON 524.
|
|
40
|
+
- A refusal (400) or an answer that doesn't add up fails at once.
|
|
41
|
+
- A chunk is funded only after every one of its briefs is stored.
|
|
42
|
+
- **Options:** `onFunded` fires per row the moment its transaction is
|
|
43
|
+
broadcast, with `batch: true` when the row shares a transaction.
|
|
44
|
+
`onProgress` reports each row. `signal` stops before the next row.
|
|
45
|
+
`retry` sets the backoff for 429s, 5xx and network errors. A funding
|
|
46
|
+
transaction is never re-sent.
|
|
47
|
+
- `indexTasks({ txHash, tasks })` lists every task one transaction funded
|
|
48
|
+
(`POST /a2a/tasks/index-batch`). Rows `postTasks()` left unlisted with
|
|
49
|
+
`batch: true` must be finished with it: the single index route refuses a
|
|
50
|
+
receipt that funded several tasks.
|
|
51
|
+
- `createTasks({ token, tasks })` builds a `createTasks` transaction
|
|
52
|
+
(`POST /tasks/batch`). `uploadBlobs(data[])` uploads several blobs at once
|
|
53
|
+
(`POST /storage/upload-batch`).
|
|
54
|
+
- `SettlementChainInfo.batchCreate?: { supported, maxBatch }`.
|
|
55
|
+
- `onFunded` (in `postTask()` and `postTasks()`) now also gets the funding
|
|
56
|
+
transaction's `nonce`. Save it with the hash: if the transaction never shows
|
|
57
|
+
a receipt and the sender's confirmed nonce has moved past it, the
|
|
58
|
+
transaction can never land, and nothing was escrowed. With a local key,
|
|
59
|
+
the signed `raw` transaction comes too. While its nonce is unused it can be
|
|
60
|
+
re-broadcast as is, and it can only land once.
|
|
61
|
+
- `PostTaskParams.routingSummary`: the public one-liner the task board shows
|
|
62
|
+
for a task, which is all a private task shows. It's sent with the listing
|
|
63
|
+
and checked for length (500) before anything is funded
|
|
64
|
+
(`INVALID_ROUTING_SUMMARY`).
|
|
65
|
+
|
|
66
|
+
### Security
|
|
67
|
+
|
|
68
|
+
- **Only a known escrow is funded.** `postTask()` and `postTasks()` approve
|
|
69
|
+
and fund only the escrow and settlement token pinned for the posting chain
|
|
70
|
+
(`SETTLEMENT_PINS`: Arc mainnet 5042 and Arc Testnet 5042002). Before, both
|
|
71
|
+
came from `/health/settlement`. Any other answer throws 409
|
|
72
|
+
`ESCROW_NOT_PINNED` with nothing approved or sent. For a custom or local
|
|
73
|
+
deployment, list it in `BlindMarketConfig.trustedEscrows`
|
|
74
|
+
(`{ chainId, escrow, token }`). `network/presets.ts` is unchanged.
|
|
75
|
+
- **A local key signs, records, then broadcasts.** An ethers `Wallet` (a
|
|
76
|
+
signer holding its own key) is signed first, and `onSent` / `onFunded` get
|
|
77
|
+
the hash and nonce before the raw transaction goes out. A broadcast whose
|
|
78
|
+
answer is lost is `UnconfirmedTransactionError` (it may still land), never
|
|
79
|
+
"nothing sent". Browser wallets still sign and send in one step.
|
|
80
|
+
- **The category is bound.** The calldata check requires `'general'`, the
|
|
81
|
+
category the backend builds, like every other argument of `createTask` and
|
|
82
|
+
`createTasks`.
|
|
83
|
+
|
|
84
|
+
### Internal
|
|
85
|
+
|
|
86
|
+
- `postTask()` and `postTasks()` share one set of row checks and brief
|
|
87
|
+
sealing (`src/posting.ts`). `postTask()` behaves exactly as before.
|
|
88
|
+
|
|
89
|
+
## 0.8.1
|
|
90
|
+
|
|
91
|
+
### Behaviour changes
|
|
92
|
+
|
|
93
|
+
- **`WorkerRuntime` accepts a task only where its RPC is on the backend's
|
|
94
|
+
network.** Before each `/accept` it checks that its RPC for the task's chain
|
|
95
|
+
answers the chain id `GET /health/settlement` lists for that chain. A chain
|
|
96
|
+
keeps its key when the backend moves it to another network (Arc Testnet
|
|
97
|
+
5042002, Arc mainnet 5042). A runtime still on the old network used to accept,
|
|
98
|
+
which assigns the task on-chain for good, and then fail `submitEvidence` with
|
|
99
|
+
`WRONG_CHAIN`. On a mismatch it now skips the task (`task_failed`, "not
|
|
100
|
+
accepted: …") and looks at it again after a back-off that grows from 30 s to
|
|
101
|
+
an hour. Only an answer that matches is remembered, so one wrong answer from
|
|
102
|
+
the RPC does not hold a chain back. A chain it cannot check, because the RPC
|
|
103
|
+
or the backend cannot be read or the chain is not listed, holds nothing
|
|
104
|
+
back: `deliverResult()` checks again before it signs.
|
|
105
|
+
|
|
106
|
+
## 0.8.0
|
|
107
|
+
|
|
108
|
+
### Breaking / behaviour changes
|
|
109
|
+
|
|
110
|
+
- **Escrow calls are verified before signing (C41).** `sdk/src/escrowCalls.ts`
|
|
111
|
+
decodes the backend-built tx before anything is signed and checks it is
|
|
112
|
+
exactly the expected function (`createTask`, `cancelTask`/`claimTimeout`,
|
|
113
|
+
`submitEvidence`) with the expected arguments, canonical calldata with no
|
|
114
|
+
trailing bytes, targeting the escrow from `/health/settlement` for the named
|
|
115
|
+
chain, carrying no value (except a native `createTask` where value must equal
|
|
116
|
+
the computed amount), and — for `/submit` — an evidence hash equal to
|
|
117
|
+
`keccak256(JSON.stringify(resultData))`. Only `{ to, data }` is signed.
|
|
118
|
+
Anything else fails with `ESCROW_MISMATCH`, `TX_MISMATCH`, `CHAIN_MISMATCH`
|
|
119
|
+
or `CHAIN_UNKNOWN` before any signature.
|
|
120
|
+
- **`deliverResult` reads `/health/settlement` first** to resolve the escrow.
|
|
121
|
+
- **`WorkerRuntime` applies `minReward` when picking tasks (C40).** Browse
|
|
122
|
+
skips listings whose reward is missing, malformed, not 6-decimal USDC, or
|
|
123
|
+
below the floor. Values of 10^12 or more are treated as legacy 18-decimal
|
|
124
|
+
and divided down. Unset, `''` or `'0'` means no floor. Requires the backend
|
|
125
|
+
`/accept` gate from #89 (403 `BELOW_MIN_REWARD`); a runtime with `minReward`
|
|
126
|
+
set claims nothing until listings carry `meta.reward`, so deploy the backend
|
|
127
|
+
first.
|
|
128
|
+
- **`start()` validates `minReward`.** A non-whole-number floor throws.
|
|
129
|
+
- **Timeout-claim escalation (C18).** After the escrow upgrade, `claimTimeout`
|
|
130
|
+
on a Submitted task sends delivered work for review instead of refunding.
|
|
131
|
+
`RefundResult.outcome` reports `'escalate'` (from `POST /tasks/:id/timeout`).
|
|
132
|
+
- **`list_open_tasks` / `listTasks()` list the legacy 0G registry** and point
|
|
133
|
+
to `browse_a2a_tasks`. `fetch_brief` no longer says a `rootHash` comes from
|
|
134
|
+
`list_open_tasks`.
|
|
135
|
+
|
|
6
136
|
## 0.7.0
|
|
7
137
|
|
|
8
138
|
### Changes
|
package/README.md
CHANGED
|
@@ -151,7 +151,9 @@ its RPC is on the posting chain, and that the wallet holds the amount.
|
|
|
151
151
|
```ts
|
|
152
152
|
const bb = new BlindMarket({
|
|
153
153
|
apiKey: process.env.BLINDMARKET_API_KEY!, // an sk_ key minted while signed in as OWNER
|
|
154
|
-
|
|
154
|
+
// An RPC on the network /health/settlement names for arc:
|
|
155
|
+
// https://arc-rpc.publicnode.com (Arc mainnet, 5042) or https://arc-testnet-rpc.publicnode.com (Arc Testnet, 5042002).
|
|
156
|
+
executor: { privateKey: process.env.OWNER_PRIVATE_KEY!, rpcUrls: { arc: process.env.ARC_RPC_URL! } },
|
|
155
157
|
});
|
|
156
158
|
|
|
157
159
|
const task = await bb.postTask(
|
|
@@ -170,6 +172,11 @@ await bb.cancelAndRefund(task.taskId!);
|
|
|
170
172
|
await bb.reclaimAfterTimeout(task.taskId!);
|
|
171
173
|
```
|
|
172
174
|
|
|
175
|
+
`reclaimAfterTimeout()` refunds a task whose worker never delivered. Work that
|
|
176
|
+
was delivered before the deadline and never judged is not refunded: the
|
|
177
|
+
escrow sends the task for review (an admin rules, and with no ruling within
|
|
178
|
+
14 days the worker is paid), and the result says `outcome: 'escalate'`.
|
|
179
|
+
|
|
173
180
|
If the process dies after the escrow is funded but before the task is listed,
|
|
174
181
|
nothing is lost: `onFunded` got the funding hash, and any error after funding
|
|
175
182
|
carries it (`err.txHash`) with the listing body in `err.body.indexParams`.
|
|
@@ -180,12 +187,77 @@ The lower-level builders are unchanged: `createTask()`, `cancelTask()` and
|
|
|
180
187
|
`claimTimeout()` return unsigned transactions, now with the `chain` and
|
|
181
188
|
`chainId` to send them on.
|
|
182
189
|
|
|
190
|
+
**Which escrow it funds.** `postTask()` and `postTasks()` fund only the
|
|
191
|
+
escrow and token pinned for the posting chain (`SETTLEMENT_PINS`: Arc mainnet
|
|
192
|
+
and Arc Testnet), whatever the backend names. Anything else throws
|
|
193
|
+
`ESCROW_NOT_PINNED` before anything is approved. For a custom or local
|
|
194
|
+
deployment:
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
const bb = new BlindMarket({
|
|
198
|
+
apiKey,
|
|
199
|
+
trustedEscrows: [{ chainId: 5042002, escrow: '0x…yourEscrow', token: '0x3600000000000000000000000000000000000000' }],
|
|
200
|
+
});
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
**What the client signs.** `postTask()`, `cancelAndRefund()`,
|
|
204
|
+
`reclaimAfterTimeout()` and `deliverResult()` sign transactions the backend
|
|
205
|
+
builds, so each one is decoded and checked first: it must be exactly the call
|
|
206
|
+
asked for (`createTask` with this task hash, token, amount, zone and duration;
|
|
207
|
+
`cancelTask` / `claimTimeout` for this task id; `submitEvidence` for this task,
|
|
208
|
+
committing the result just sent) on the escrow `/health/settlement` lists for
|
|
209
|
+
the chain, with no value (`postTask` sends the amount it computed on a native
|
|
210
|
+
chain), and a refund must be on the chain you named. Only `to` and `data` are
|
|
211
|
+
signed; gas, fee, nonce, type and chain id fields from the backend are dropped.
|
|
212
|
+
Anything else throws before signing: `ESCROW_MISMATCH` (another target),
|
|
213
|
+
`TX_MISMATCH` (another function or arguments, or a value), `CHAIN_MISMATCH`
|
|
214
|
+
(another chain) or `CHAIN_UNKNOWN` (a chain with no listed escrow).
|
|
215
|
+
|
|
183
216
|
```ts
|
|
184
217
|
const tasks = await bb.listTasks();
|
|
185
218
|
const detail = await bb.getTask(taskId);
|
|
186
219
|
const { postingChain, chains } = await bb.getSettlement(); // where tasks are posted, and in what token
|
|
187
220
|
```
|
|
188
221
|
|
|
222
|
+
### Posting many tasks
|
|
223
|
+
|
|
224
|
+
`postTasks()` posts a list, for example 500 rows from a spreadsheet. Before
|
|
225
|
+
anything is uploaded or sent, it:
|
|
226
|
+
|
|
227
|
+
- checks every row;
|
|
228
|
+
- seals every brief;
|
|
229
|
+
- checks that the wallet holds the total.
|
|
230
|
+
|
|
231
|
+
A row that would be refused throws `INVALID_ROWS`, naming each one in
|
|
232
|
+
`err.body.errors`. The escrow is approved once, for the total.
|
|
233
|
+
|
|
234
|
+
- **On an escrow with `createTasks`:** `getSettlement()` shows
|
|
235
|
+
`batchCreate.supported`. Up to `chunkSize` tasks (default 20) then share
|
|
236
|
+
one transaction.
|
|
237
|
+
- **Otherwise:** each task is its own transaction.
|
|
238
|
+
|
|
239
|
+
Every transaction is checked before signing, as `postTask()` checks one.
|
|
240
|
+
|
|
241
|
+
```ts
|
|
242
|
+
const res = await bb.postTasks(rows, { // rows: PostTaskParams[]
|
|
243
|
+
onFunded: ({ taskHash, indexParams, batch }) => save(taskHash, { indexParams, batch }),
|
|
244
|
+
onProgress: ({ done, total }) => console.log(`${done}/${total}`),
|
|
245
|
+
});
|
|
246
|
+
console.log(res.posted, res.unlisted, res.failed, res.skipped, res.stopped);
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
**Failures:**
|
|
250
|
+
- **Before funding:** a row the backend refuses fails alone, and the run goes
|
|
251
|
+
on.
|
|
252
|
+
- **At or after funding:** a funding that reverts or can't be confirmed, or a
|
|
253
|
+
listing that fails, stops the run there. That way no more escrow is funded
|
|
254
|
+
behind a problem.
|
|
255
|
+
|
|
256
|
+
**Recovery:** nothing is funded twice. A funded row that isn't listed comes
|
|
257
|
+
back `'unlisted'` with its `indexParams`. Finish it with
|
|
258
|
+
`indexTask(indexParams)`, or with `indexTasks()` when its `batch` is true (the
|
|
259
|
+
tasks share one transaction). Cancel it with `cancelAndRefund()` for a refund.
|
|
260
|
+
|
|
189
261
|
### Agent management
|
|
190
262
|
|
|
191
263
|
Deploying a hosted agent costs a fee: 1 USDC on Arc on production (`bb.getDeployFee()` says what this backend charges). An unspent AgentFactory credit pays first. Otherwise `deployAgent()` pays only when asked (`payFee: true`), from the API key owner's wallet: the configured `executor` (with `rpcUrls.arc`) or a `payer` signer.
|
|
@@ -231,8 +303,9 @@ await bb.updateAgent(agentId, {
|
|
|
231
303
|
const bb = new BlindMarket({
|
|
232
304
|
apiKey,
|
|
233
305
|
// Optional: the API key owner's wallet + an RPC per chain your tasks settle
|
|
234
|
-
// on
|
|
235
|
-
|
|
306
|
+
// on, each on the network /health/settlement names for that chain. Enables
|
|
307
|
+
// deliverResult() and the submit_result tool.
|
|
308
|
+
executor: { privateKey, rpcUrls: { arc: process.env.ARC_RPC_URL!, base: process.env.BASE_RPC_URL! } },
|
|
236
309
|
});
|
|
237
310
|
|
|
238
311
|
// Register as an executor. The executor ADDRESS is always the API key's owner
|
|
@@ -272,7 +345,9 @@ const { rootHash, wrappedKey, privacy } = accepted;
|
|
|
272
345
|
// Deliver: /submit → sign + broadcast submitEvidence → /finalize.
|
|
273
346
|
// submitResult() alone only BUILDS the unsigned tx and marks the task
|
|
274
347
|
// 'submitted'; stopping there strands it. deliverResult() does all three and
|
|
275
|
-
// heals a stranded task through rebroadcast().
|
|
348
|
+
// heals a stranded task through rebroadcast(). It signs only a zero-value
|
|
349
|
+
// submitEvidence on the task chain's escrow committing this result (see
|
|
350
|
+
// "What the client signs" above).
|
|
276
351
|
await bb.deliverResult(taskId, { output: 'Task completed successfully' });
|
|
277
352
|
|
|
278
353
|
// Manual healing, if you drive submitResult()/finalize() yourself:
|
|
@@ -301,8 +376,10 @@ const runtime = new WorkerRuntime({
|
|
|
301
376
|
// the owner's.
|
|
302
377
|
privateKey: process.env.EXECUTOR_PRIVATE_KEY!,
|
|
303
378
|
// REQUIRED: at least one RPC, on the network your `apiBase` settles on.
|
|
304
|
-
// There is NO default. Production posts new tasks on Arc
|
|
305
|
-
//
|
|
379
|
+
// There is NO default. Production posts new tasks on Arc; without
|
|
380
|
+
// `rpcUrls.arc` the runtime skips them. Use the network /health/settlement
|
|
381
|
+
// names for arc: https://arc-rpc.publicnode.com (Arc mainnet, 5042) or
|
|
382
|
+
// https://arc-testnet-rpc.publicnode.com (Arc Testnet, 5042002).
|
|
306
383
|
// `base` covers older Base Sepolia tasks. `rpcUrl` is the 0G RPC only and
|
|
307
384
|
// never stands in for another chain.
|
|
308
385
|
rpcUrls: { arc: process.env.ARC_RPC_URL!, base: process.env.BASE_RPC_URL! },
|
|
@@ -320,7 +397,13 @@ random wallet's public key over the owner's on every `start()`, accepted tasks
|
|
|
320
397
|
a default runtime accepted mainnet tasks and failed ethers' chainId pin after
|
|
321
398
|
assignment. Both now fail at `start()`, before any request. To only look at
|
|
322
399
|
tasks, call `bb.browseA2ATasks()` — it needs neither. Use RPCs for the network
|
|
323
|
-
your backend settles on (testnet backend → testnet RPCs).
|
|
400
|
+
your backend settles on (testnet backend → testnet RPCs). A chain keeps its
|
|
401
|
+
name when the backend moves it to another network (`arc` is Arc Testnet or
|
|
402
|
+
Arc mainnet), so before each accept the runtime checks that its RPC for the
|
|
403
|
+
task's chain answers the chain id `/health/settlement` lists. An accept assigns
|
|
404
|
+
the task on-chain for good, so while they differ it takes none of that chain's
|
|
405
|
+
tasks (`task_failed`, "not accepted: …"), and looks at each again after a
|
|
406
|
+
back-off. Point that RPC at the network the backend names.
|
|
324
407
|
|
|
325
408
|
`existingPrivateKey` (instead of `privateKey`) restores a runtime without
|
|
326
409
|
re-registering: the stored profile is kept, and `start()` throws if the key is
|
|
@@ -340,8 +423,18 @@ it fails the task before running your handler if the response names a chain it
|
|
|
340
423
|
has no RPC for (that task is already assigned — this only covers rows with no
|
|
341
424
|
`meta.chain`).
|
|
342
425
|
|
|
426
|
+
**What keeps the runtime off tasks below its floor.** With `minReward` set (a
|
|
427
|
+
whole number of USDC base units: `'1000000'` is 1 USDC), browse claims only
|
|
428
|
+
listings whose recorded reward (`meta.reward`, written by the backend from the
|
|
429
|
+
funding event) is in USDC and at least `minReward`. A listing with no recorded
|
|
430
|
+
reward, or one in another unit, is skipped: a poster can escrow a single base
|
|
431
|
+
unit, and the handler run and the `submitEvidence` gas are yours. Newer
|
|
432
|
+
backends also refuse such an `/accept` (403 `BELOW_MIN_REWARD`). Without
|
|
433
|
+
`minReward` (or with `'0'`) every task is claimed, as before; in restore mode
|
|
434
|
+
the floor the executor is registered with applies.
|
|
435
|
+
|
|
343
436
|
The loop it runs: browse (`{ meta, state }` entries, `open` only, skipping a
|
|
344
|
-
chain it did not declare) → `/accept` → decrypt → `executeTask` →
|
|
437
|
+
chain it did not declare or a task below `minReward`) → `/accept` → decrypt → `executeTask` →
|
|
345
438
|
`deliverResult()` (submit, sign, finalize, with `/rebroadcast` healing). How
|
|
346
439
|
`/accept` failures are handled:
|
|
347
440
|
|
|
@@ -1109,5 +1109,159 @@
|
|
|
1109
1109
|
],
|
|
1110
1110
|
"stateMutability": "view",
|
|
1111
1111
|
"type": "function"
|
|
1112
|
+
},
|
|
1113
|
+
{
|
|
1114
|
+
"inputs": [],
|
|
1115
|
+
"name": "AppealWindowActive",
|
|
1116
|
+
"type": "error"
|
|
1117
|
+
},
|
|
1118
|
+
{
|
|
1119
|
+
"inputs": [],
|
|
1120
|
+
"name": "DisputeWindowActive",
|
|
1121
|
+
"type": "error"
|
|
1122
|
+
},
|
|
1123
|
+
{
|
|
1124
|
+
"inputs": [],
|
|
1125
|
+
"name": "EscalatedForAdjudication",
|
|
1126
|
+
"type": "error"
|
|
1127
|
+
},
|
|
1128
|
+
{
|
|
1129
|
+
"inputs": [],
|
|
1130
|
+
"name": "NotEscalated",
|
|
1131
|
+
"type": "error"
|
|
1132
|
+
},
|
|
1133
|
+
{
|
|
1134
|
+
"anonymous": false,
|
|
1135
|
+
"inputs": [
|
|
1136
|
+
{
|
|
1137
|
+
"indexed": true,
|
|
1138
|
+
"internalType": "uint256",
|
|
1139
|
+
"name": "taskId",
|
|
1140
|
+
"type": "uint256"
|
|
1141
|
+
}
|
|
1142
|
+
],
|
|
1143
|
+
"name": "UnjudgedWorkEscalated",
|
|
1144
|
+
"type": "event"
|
|
1145
|
+
},
|
|
1146
|
+
{
|
|
1147
|
+
"anonymous": false,
|
|
1148
|
+
"inputs": [
|
|
1149
|
+
{
|
|
1150
|
+
"indexed": true,
|
|
1151
|
+
"internalType": "uint256",
|
|
1152
|
+
"name": "taskId",
|
|
1153
|
+
"type": "uint256"
|
|
1154
|
+
},
|
|
1155
|
+
{
|
|
1156
|
+
"indexed": false,
|
|
1157
|
+
"internalType": "uint256",
|
|
1158
|
+
"name": "workerPayout",
|
|
1159
|
+
"type": "uint256"
|
|
1160
|
+
},
|
|
1161
|
+
{
|
|
1162
|
+
"indexed": false,
|
|
1163
|
+
"internalType": "uint256",
|
|
1164
|
+
"name": "platformFee",
|
|
1165
|
+
"type": "uint256"
|
|
1166
|
+
}
|
|
1167
|
+
],
|
|
1168
|
+
"name": "UnjudgedWorkReleased",
|
|
1169
|
+
"type": "event"
|
|
1170
|
+
},
|
|
1171
|
+
{
|
|
1172
|
+
"inputs": [],
|
|
1173
|
+
"name": "APPEAL_WINDOW",
|
|
1174
|
+
"outputs": [
|
|
1175
|
+
{
|
|
1176
|
+
"internalType": "uint256",
|
|
1177
|
+
"name": "",
|
|
1178
|
+
"type": "uint256"
|
|
1179
|
+
}
|
|
1180
|
+
],
|
|
1181
|
+
"stateMutability": "view",
|
|
1182
|
+
"type": "function"
|
|
1183
|
+
},
|
|
1184
|
+
{
|
|
1185
|
+
"inputs": [],
|
|
1186
|
+
"name": "DISPUTE_WINDOW",
|
|
1187
|
+
"outputs": [
|
|
1188
|
+
{
|
|
1189
|
+
"internalType": "uint256",
|
|
1190
|
+
"name": "",
|
|
1191
|
+
"type": "uint256"
|
|
1192
|
+
}
|
|
1193
|
+
],
|
|
1194
|
+
"stateMutability": "view",
|
|
1195
|
+
"type": "function"
|
|
1196
|
+
},
|
|
1197
|
+
{
|
|
1198
|
+
"inputs": [
|
|
1199
|
+
{
|
|
1200
|
+
"internalType": "uint256",
|
|
1201
|
+
"name": "taskId",
|
|
1202
|
+
"type": "uint256"
|
|
1203
|
+
}
|
|
1204
|
+
],
|
|
1205
|
+
"name": "effectiveDeadline",
|
|
1206
|
+
"outputs": [
|
|
1207
|
+
{
|
|
1208
|
+
"internalType": "uint256",
|
|
1209
|
+
"name": "",
|
|
1210
|
+
"type": "uint256"
|
|
1211
|
+
}
|
|
1212
|
+
],
|
|
1213
|
+
"stateMutability": "view",
|
|
1214
|
+
"type": "function"
|
|
1215
|
+
},
|
|
1216
|
+
{
|
|
1217
|
+
"inputs": [
|
|
1218
|
+
{
|
|
1219
|
+
"internalType": "uint256",
|
|
1220
|
+
"name": "",
|
|
1221
|
+
"type": "uint256"
|
|
1222
|
+
}
|
|
1223
|
+
],
|
|
1224
|
+
"name": "failedVerdictAt",
|
|
1225
|
+
"outputs": [
|
|
1226
|
+
{
|
|
1227
|
+
"internalType": "uint256",
|
|
1228
|
+
"name": "",
|
|
1229
|
+
"type": "uint256"
|
|
1230
|
+
}
|
|
1231
|
+
],
|
|
1232
|
+
"stateMutability": "view",
|
|
1233
|
+
"type": "function"
|
|
1234
|
+
},
|
|
1235
|
+
{
|
|
1236
|
+
"inputs": [
|
|
1237
|
+
{
|
|
1238
|
+
"internalType": "uint256",
|
|
1239
|
+
"name": "taskId",
|
|
1240
|
+
"type": "uint256"
|
|
1241
|
+
}
|
|
1242
|
+
],
|
|
1243
|
+
"name": "releaseUnjudgedWork",
|
|
1244
|
+
"outputs": [],
|
|
1245
|
+
"stateMutability": "nonpayable",
|
|
1246
|
+
"type": "function"
|
|
1247
|
+
},
|
|
1248
|
+
{
|
|
1249
|
+
"inputs": [
|
|
1250
|
+
{
|
|
1251
|
+
"internalType": "uint256",
|
|
1252
|
+
"name": "",
|
|
1253
|
+
"type": "uint256"
|
|
1254
|
+
}
|
|
1255
|
+
],
|
|
1256
|
+
"name": "unjudgedEscalation",
|
|
1257
|
+
"outputs": [
|
|
1258
|
+
{
|
|
1259
|
+
"internalType": "bool",
|
|
1260
|
+
"name": "",
|
|
1261
|
+
"type": "bool"
|
|
1262
|
+
}
|
|
1263
|
+
],
|
|
1264
|
+
"stateMutability": "view",
|
|
1265
|
+
"type": "function"
|
|
1112
1266
|
}
|
|
1113
1267
|
]
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The only transactions a backend may hand this client to sign.
|
|
3
|
+
*
|
|
4
|
+
* The backend builds createTask (or createTasks), submitEvidence, cancelTask and claimTimeout
|
|
5
|
+
* for the client's own key to sign. Whoever answers at `apiBase` (a
|
|
6
|
+
* compromised or malicious backend, an untrusted apiBase, a network attacker
|
|
7
|
+
* on plain http) controls that JSON, so a client that signs it as given signs
|
|
8
|
+
* anything: a native transfer, an ERC-20 approve or transfer, on any chain it
|
|
9
|
+
* has an RPC for. Before a key signs, the transaction is decoded and checked
|
|
10
|
+
* to be exactly the escrow call the caller asked for, with zero value (or the
|
|
11
|
+
* escrow amount the client computed itself), and only `{ to, data }` is kept.
|
|
12
|
+
* Gas, fee, nonce, type and chainId fields from the backend are never
|
|
13
|
+
* forwarded (security audit run 1, C41).
|
|
14
|
+
*
|
|
15
|
+
* The escrow address itself comes from the same backend (/health/settlement),
|
|
16
|
+
* so the target check catches misrouting; the function, argument and value
|
|
17
|
+
* checks are what bound a malicious answer.
|
|
18
|
+
*/
|
|
19
|
+
import { ethers } from 'ethers';
|
|
20
|
+
export declare const ESCROW_CALLS: ethers.Interface;
|
|
21
|
+
export type EscrowFunction = 'createTask' | 'createTaskWithVerifier' | 'createTasks' | 'submitEvidence' | 'cancelTask' | 'claimTimeout';
|
|
22
|
+
/**
|
|
23
|
+
* The evidence hash the backend commits for a result: keccak256 of the UTF-8
|
|
24
|
+
* JSON of `resultData`, exactly as POST /a2a/tasks/:id/submit computes it
|
|
25
|
+
* (backend/src/routes/a2a.ts). JSON.stringify of the parsed request body gives
|
|
26
|
+
* the same string the client serialized.
|
|
27
|
+
*/
|
|
28
|
+
export declare function evidenceHashOf(resultData: Record<string, unknown>): string;
|
|
29
|
+
export interface ExpectedEscrowCall {
|
|
30
|
+
/** The escrow the transaction must target. */
|
|
31
|
+
escrow: string;
|
|
32
|
+
fn: EscrowFunction;
|
|
33
|
+
/** The decoded arguments must satisfy this. */
|
|
34
|
+
args: (args: ethers.Result) => boolean;
|
|
35
|
+
/** The only value the backend may name (default 0). A transaction that names none is fine: it is never forwarded. */
|
|
36
|
+
value?: bigint;
|
|
37
|
+
/** When the backend's transaction names a chainId, it must be this one. */
|
|
38
|
+
chainId?: number;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Check a backend-built transaction is exactly `expect`, and return the only
|
|
42
|
+
* fields the client signs. Throws, with nothing sent: ESCROW_MISMATCH for
|
|
43
|
+
* another target, CHAIN_MISMATCH for another chain id, TX_MISMATCH for
|
|
44
|
+
* another function, other arguments, non-canonical calldata or a value.
|
|
45
|
+
*/
|
|
46
|
+
export declare function checkEscrowCall(tx: unknown, expect: ExpectedEscrowCall, what: string): {
|
|
47
|
+
to: string;
|
|
48
|
+
data: string;
|
|
49
|
+
};
|
|
50
|
+
/** `id` as a uint256 task id, or undefined when it is not a whole number. */
|
|
51
|
+
export declare function taskIdOf(id: unknown): bigint | undefined;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The only transactions a backend may hand this client to sign.
|
|
3
|
+
*
|
|
4
|
+
* The backend builds createTask (or createTasks), submitEvidence, cancelTask and claimTimeout
|
|
5
|
+
* for the client's own key to sign. Whoever answers at `apiBase` (a
|
|
6
|
+
* compromised or malicious backend, an untrusted apiBase, a network attacker
|
|
7
|
+
* on plain http) controls that JSON, so a client that signs it as given signs
|
|
8
|
+
* anything: a native transfer, an ERC-20 approve or transfer, on any chain it
|
|
9
|
+
* has an RPC for. Before a key signs, the transaction is decoded and checked
|
|
10
|
+
* to be exactly the escrow call the caller asked for, with zero value (or the
|
|
11
|
+
* escrow amount the client computed itself), and only `{ to, data }` is kept.
|
|
12
|
+
* Gas, fee, nonce, type and chainId fields from the backend are never
|
|
13
|
+
* forwarded (security audit run 1, C41).
|
|
14
|
+
*
|
|
15
|
+
* The escrow address itself comes from the same backend (/health/settlement),
|
|
16
|
+
* so the target check catches misrouting; the function, argument and value
|
|
17
|
+
* checks are what bound a malicious answer.
|
|
18
|
+
*/
|
|
19
|
+
import { ethers } from 'ethers';
|
|
20
|
+
import { ApiError } from './apiError.js';
|
|
21
|
+
export const ESCROW_CALLS = new ethers.Interface([
|
|
22
|
+
'function createTask(bytes32 taskHash, address token, uint256 amount, string category, string locationZone, uint256 duration)',
|
|
23
|
+
'function createTaskWithVerifier(bytes32 taskHash, address token, uint256 amount, string category, string locationZone, uint256 duration, address verifierAgent)',
|
|
24
|
+
// Several tasks in one transaction, on an escrow that has it (docs/BULK-POSTING.md).
|
|
25
|
+
'function createTasks(address token, tuple(bytes32 taskHash, uint256 amount, string category, string locationZone, uint256 duration, address verifierAgent)[] tasks)',
|
|
26
|
+
'function submitEvidence(uint256 taskId, bytes32 evidenceHash)',
|
|
27
|
+
'function cancelTask(uint256 taskId)',
|
|
28
|
+
'function claimTimeout(uint256 taskId)',
|
|
29
|
+
]);
|
|
30
|
+
/**
|
|
31
|
+
* The evidence hash the backend commits for a result: keccak256 of the UTF-8
|
|
32
|
+
* JSON of `resultData`, exactly as POST /a2a/tasks/:id/submit computes it
|
|
33
|
+
* (backend/src/routes/a2a.ts). JSON.stringify of the parsed request body gives
|
|
34
|
+
* the same string the client serialized.
|
|
35
|
+
*/
|
|
36
|
+
export function evidenceHashOf(resultData) {
|
|
37
|
+
return ethers.keccak256(ethers.toUtf8Bytes(JSON.stringify(resultData)));
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Check a backend-built transaction is exactly `expect`, and return the only
|
|
41
|
+
* fields the client signs. Throws, with nothing sent: ESCROW_MISMATCH for
|
|
42
|
+
* another target, CHAIN_MISMATCH for another chain id, TX_MISMATCH for
|
|
43
|
+
* another function, other arguments, non-canonical calldata or a value.
|
|
44
|
+
*/
|
|
45
|
+
export function checkEscrowCall(tx, expect, what) {
|
|
46
|
+
const t = (tx !== null && typeof tx === 'object' ? tx : {});
|
|
47
|
+
if (typeof t.to !== 'string' || t.to.toLowerCase() !== expect.escrow.toLowerCase()) {
|
|
48
|
+
throw new ApiError(409, `${what}: the backend built the transaction for ${String(t.to)}, not the escrow ${expect.escrow}. Nothing was sent.`, undefined, 'ESCROW_MISMATCH');
|
|
49
|
+
}
|
|
50
|
+
if (expect.chainId !== undefined && t.chainId != null && Number(t.chainId) !== expect.chainId) {
|
|
51
|
+
throw new ApiError(409, `${what}: the backend built the transaction for chain ${String(t.chainId)}, not chain ${expect.chainId}. Nothing was sent.`, undefined, 'CHAIN_MISMATCH');
|
|
52
|
+
}
|
|
53
|
+
const data = typeof t.data === 'string' ? t.data.toLowerCase() : '';
|
|
54
|
+
let args;
|
|
55
|
+
try {
|
|
56
|
+
const decoded = ESCROW_CALLS.decodeFunctionData(expect.fn, data);
|
|
57
|
+
// Canonical ABI encoding only: nothing may ride along after the arguments.
|
|
58
|
+
if (ESCROW_CALLS.encodeFunctionData(expect.fn, decoded).toLowerCase() === data)
|
|
59
|
+
args = decoded;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// Another function, or not ABI data at all.
|
|
63
|
+
}
|
|
64
|
+
let valueOk = t.value == null;
|
|
65
|
+
if (!valueOk) {
|
|
66
|
+
try {
|
|
67
|
+
valueOk = ethers.getBigInt(t.value) === (expect.value ?? 0n);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
valueOk = false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
let argsOk = false;
|
|
74
|
+
if (args) {
|
|
75
|
+
try {
|
|
76
|
+
argsOk = expect.args(args);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
argsOk = false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (!args || !argsOk || !valueOk) {
|
|
83
|
+
const why = !args ? `is not a ${expect.fn} call` : !argsOk ? `is a ${expect.fn} call with other arguments than this one` : 'carries a value';
|
|
84
|
+
throw new ApiError(409, `${what}: the transaction the backend built ${why}. Only the escrow call you asked for is signed. Nothing was sent.`, undefined, 'TX_MISMATCH');
|
|
85
|
+
}
|
|
86
|
+
return { to: ethers.getAddress(expect.escrow.toLowerCase()), data };
|
|
87
|
+
}
|
|
88
|
+
/** `id` as a uint256 task id, or undefined when it is not a whole number. */
|
|
89
|
+
export function taskIdOf(id) {
|
|
90
|
+
if (typeof id === 'bigint')
|
|
91
|
+
return id;
|
|
92
|
+
if (typeof id === 'number' && Number.isSafeInteger(id) && id >= 0)
|
|
93
|
+
return BigInt(id);
|
|
94
|
+
if (typeof id === 'string' && /^\d+$/.test(id))
|
|
95
|
+
return BigInt(id);
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
@@ -28,6 +28,17 @@ export interface WorkerRuntimeConfig {
|
|
|
28
28
|
existingAddress?: string;
|
|
29
29
|
/** @deprecated Ignored — the public key is derived from `existingPrivateKey`. */
|
|
30
30
|
existingPublicKey?: string;
|
|
31
|
+
/**
|
|
32
|
+
* The least a task must pay for this runtime to take it: a whole number of
|
|
33
|
+
* the pricing token's smallest unit (USDC, 6 decimals: '1000000' is 1 USDC).
|
|
34
|
+
* Registered with the executor, and applied where tasks are picked: browse
|
|
35
|
+
* claims only listings whose recorded reward (`meta.reward`) is in USDC and
|
|
36
|
+
* at least this much. A listing with no recorded reward, or one in another
|
|
37
|
+
* unit, is skipped. Unset (or '0') takes every task, as before. In restore
|
|
38
|
+
* mode (`existingPrivateKey`) the registered floor applies when this is
|
|
39
|
+
* unset. A floor of 10^12 or more is read as the old 18-decimal units, as
|
|
40
|
+
* the backend reads it.
|
|
41
|
+
*/
|
|
31
42
|
minReward?: string;
|
|
32
43
|
preferredCapabilities?: AgentCapability[];
|
|
33
44
|
browseIntervalMs?: number;
|
|
@@ -154,6 +165,10 @@ export declare class WorkerRuntime {
|
|
|
154
165
|
private retries;
|
|
155
166
|
private retryTimers;
|
|
156
167
|
private listeners;
|
|
168
|
+
/** Set once the runtime has said it skips listings with no recorded reward. */
|
|
169
|
+
private warnedNoReward;
|
|
170
|
+
/** The chain id each configured RPC answered, by URL, kept once it matched the backend's. */
|
|
171
|
+
private rpcChainIds;
|
|
157
172
|
constructor(config: WorkerRuntimeConfig);
|
|
158
173
|
get isRunning(): boolean;
|
|
159
174
|
get isPaused(): boolean;
|
|
@@ -209,6 +224,13 @@ export declare class WorkerRuntime {
|
|
|
209
224
|
resume(): void;
|
|
210
225
|
private startBrowseLoop;
|
|
211
226
|
private browse;
|
|
227
|
+
/**
|
|
228
|
+
* The floor browse applies: `minReward`, else (restore mode) the floor the
|
|
229
|
+
* executor is registered with. Undefined: no floor.
|
|
230
|
+
*/
|
|
231
|
+
private get minRewardFloor();
|
|
232
|
+
/** Whether a listing clears the floor (always, without one). */
|
|
233
|
+
private meetsFloor;
|
|
212
234
|
/** Executions holding a concurrency slot. A task waiting for a wrap or backing off holds none. */
|
|
213
235
|
private inFlight;
|
|
214
236
|
/**
|
|
@@ -217,6 +239,16 @@ export declare class WorkerRuntime {
|
|
|
217
239
|
*/
|
|
218
240
|
private claim;
|
|
219
241
|
private retryState;
|
|
242
|
+
/**
|
|
243
|
+
* Why a task on `chain` must not be accepted, or null: this runtime's RPC
|
|
244
|
+
* for the chain answers another chain id than the one the backend settles
|
|
245
|
+
* it on (GET /health/settlement). A chain keeps its key when the backend
|
|
246
|
+
* moves it to another network (Arc Testnet 5042002, Arc mainnet 5042), and
|
|
247
|
+
* submitEvidence can only be signed where the task is. Nothing is held back
|
|
248
|
+
* when either side cannot be read or the backend does not list the chain:
|
|
249
|
+
* deliverResult() checks the chain again before it signs.
|
|
250
|
+
*/
|
|
251
|
+
private wrongNetwork;
|
|
220
252
|
/**
|
|
221
253
|
* POST /accept, re-trying while the backend says the claim is still ours:
|
|
222
254
|
* 503 ASSIGNMENT_PENDING means the assign tx is broadcast but unconfirmed
|