@blindmarket/mcp-server 0.3.5 → 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/README.md +39 -13
- package/dist/index.js +29 -12
- package/dist/index.js.map +1 -1
- package/dist/rent.d.ts.map +1 -1
- package/dist/rent.js +153 -60
- package/dist/rent.js.map +1 -1
- package/dist/settlement.d.ts +63 -13
- package/dist/settlement.d.ts.map +1 -1
- package/dist/settlement.js +105 -14
- package/dist/settlement.js.map +1 -1
- package/dist/state.d.ts +3 -2
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js.map +1 -1
- package/dist/tools.d.ts +12 -1
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +85 -43
- package/dist/tools.js.map +1 -1
- package/dist/wallet.js +3 -3
- package/dist/wallet.js.map +1 -1
- package/package.json +4 -3
package/dist/rent.js
CHANGED
|
@@ -29,6 +29,16 @@ const ESCROW_READ_ABI = [
|
|
|
29
29
|
*/
|
|
30
30
|
const ZERO_TOKEN = '0x0000000000000000000000000000000000000000';
|
|
31
31
|
const GAS_LIMIT = 1000000n; // matches the canonical rent script
|
|
32
|
+
// Auto-verify releases the payment, so the bar can't be "one character" — but
|
|
33
|
+
// 40 made a correct 30-character URL unpayable. 20 is the platform floor
|
|
34
|
+
// (DEFAULT_MIN_CONTENT_CHARS in the backend's autoVerify, where min_length is a
|
|
35
|
+
// hard floor). Accepted limit: a one-word correct answer still can't
|
|
36
|
+
// auto-verify without an expected_answer. MUST match the web app's
|
|
37
|
+
// RENTAL_VERIFICATION_CRITERIA (frontend/src/components/UseServiceModal.tsx and
|
|
38
|
+
// UseFromAgentModal.tsx) so a rental is judged the same whichever client paid
|
|
39
|
+
// for it. The index route also rejects 'auto' with no real criterion (400
|
|
40
|
+
// AUTO_CRITERIA_REQUIRED).
|
|
41
|
+
const RENTAL_VERIFICATION_CRITERIA = { min_length: 20 };
|
|
32
42
|
function ok(data) {
|
|
33
43
|
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
34
44
|
}
|
|
@@ -85,8 +95,9 @@ export function registerRentTools(server, cfg, walletCtx) {
|
|
|
85
95
|
}
|
|
86
96
|
const settlement = createSettlementResolver({ apiBase: cfg.apiBase ?? 'https://api.blindmarket.xyz', api });
|
|
87
97
|
/** How this process pays. On 0G that is the local wallet, which must exist.
|
|
88
|
-
* On Base nothing signs locally — the relay signs from the
|
|
89
|
-
* wallet — so a missing BLINDMARKET_PRIVATE_KEY is not an
|
|
98
|
+
* On a relay chain (Base) nothing signs locally — the relay signs from the
|
|
99
|
+
* API key's owner wallet — so a missing BLINDMARKET_PRIVATE_KEY is not an
|
|
100
|
+
* error there. */
|
|
90
101
|
async function requireFunding() {
|
|
91
102
|
let s;
|
|
92
103
|
try {
|
|
@@ -95,26 +106,44 @@ export function registerRentTools(server, cfg, walletCtx) {
|
|
|
95
106
|
catch (err) {
|
|
96
107
|
return { error: fail(err.code ?? 'SETTLEMENT_UNKNOWN', err.message) };
|
|
97
108
|
}
|
|
98
|
-
if (s.
|
|
109
|
+
if (s.payment === 'relay-erc20')
|
|
99
110
|
return { s, payFrom: s.payFrom };
|
|
100
111
|
if (!walletCtx) {
|
|
101
112
|
return { error: fail('NO_WALLET', 'Spending on 0G needs a local funding wallet — set BLINDMARKET_PRIVATE_KEY (see wallet_status)') };
|
|
102
113
|
}
|
|
114
|
+
// The escrow address is compared before every send, but not the chain:
|
|
115
|
+
// a testnet backend's escrow address, paid on the mainnet RPC, is some
|
|
116
|
+
// other account there. A backend that names its 0G chain id settles it.
|
|
117
|
+
if (s.chainId !== undefined && walletCtx.chainId !== s.chainId) {
|
|
118
|
+
return {
|
|
119
|
+
error: fail('CHAIN_MISMATCH', `The backend settles 0G on chain ${s.chainId}, but BLINDMARKET_PRIVATE_KEY signs on chain ${walletCtx.chainId} (BLINDMARKET_RPC_URL / BLINDMARKET_CHAIN_ID). Native value sent there would not reach this backend's escrow. Point both at chain ${s.chainId}.`),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
103
122
|
return { s, payFrom: walletCtx.wallet.address };
|
|
104
123
|
}
|
|
124
|
+
/** A new escrow is funded where POST /api/v1/tasks builds: the backend's
|
|
125
|
+
* posting chain. A process forced onto another chain (to finish or refund
|
|
126
|
+
* tasks already there) cannot post. Unknown on an older backend. */
|
|
127
|
+
function notPostingChain(s) {
|
|
128
|
+
if (s.postingChain === undefined || s.mode === s.postingChain)
|
|
129
|
+
return null;
|
|
130
|
+
const e = new Error(`This process settles on ${s.mode} (BLINDMARKET_SETTLEMENT), but the backend posts new tasks on ${s.postingChain}, so a new escrow can only be funded there. Unset BLINDMARKET_SETTLEMENT (or set it to ${s.postingChain}) to post; ${s.mode} stays usable for tasks already on it.`);
|
|
131
|
+
e.code = 'NOT_POSTING_CHAIN';
|
|
132
|
+
return e;
|
|
133
|
+
}
|
|
105
134
|
const ERC20 = new Interface([
|
|
106
135
|
'function allowance(address owner, address spender) view returns (uint256)',
|
|
107
136
|
'function approve(address spender, uint256 amount) returns (bool)',
|
|
108
137
|
'function balanceOf(address owner) view returns (uint256)',
|
|
109
138
|
]);
|
|
110
|
-
function
|
|
111
|
-
return new Contract(s.
|
|
139
|
+
function settlementToken(s) {
|
|
140
|
+
return new Contract(s.token.address, ERC20, s.provider);
|
|
112
141
|
}
|
|
113
142
|
/** Spendable balance of whoever pays, in the settlement token's units. */
|
|
114
143
|
async function payFromBalance(s, payFrom) {
|
|
115
144
|
try {
|
|
116
|
-
const raw = s.
|
|
117
|
-
? await
|
|
145
|
+
const raw = s.payment === 'relay-erc20'
|
|
146
|
+
? await settlementToken(s).balanceOf(payFrom)
|
|
118
147
|
: await walletCtx.provider.getBalance(payFrom);
|
|
119
148
|
return formatUnits(raw, s.decimals);
|
|
120
149
|
}
|
|
@@ -126,19 +155,22 @@ export function registerRentTools(server, cfg, walletCtx) {
|
|
|
126
155
|
* the task, and the tx carries no chainId. Discovery is only a hint about
|
|
127
156
|
* that chain (see settlement.ts). So before broadcasting, check the tx
|
|
128
157
|
* targets the escrow THIS mode expects — otherwise native value goes to a
|
|
129
|
-
*
|
|
130
|
-
* mismatch also drops the cached mode so the next call
|
|
158
|
+
* relay chain's address on the 0G RPC, or a 0G refund is relayed onto
|
|
159
|
+
* another chain. A mismatch also drops the cached mode so the next call
|
|
160
|
+
* re-asks. */
|
|
131
161
|
async function verifyTarget(s, to, what) {
|
|
132
162
|
const target = String(to).toLowerCase();
|
|
133
|
-
const expected = s.
|
|
163
|
+
const expected = s.escrowAddress;
|
|
134
164
|
if (expected) {
|
|
135
165
|
if (target === expected.toLowerCase())
|
|
136
166
|
return;
|
|
137
167
|
settlement.invalidate();
|
|
138
168
|
const e = new Error(`backend built ${what} for ${to} but this process is in ${s.mode} mode expecting escrow ${expected}. ` +
|
|
139
|
-
(s.
|
|
140
|
-
?
|
|
141
|
-
:
|
|
169
|
+
(s.escrowChains
|
|
170
|
+
? `This task is escrowed on another chain — set BLINDMARKET_SETTLEMENT to the chain that holds it (this backend has escrows on ${s.escrowChains.join(', ')}; 0G needs a local key), or use the web app.`
|
|
171
|
+
: s.payment === 'relay-erc20'
|
|
172
|
+
? 'This task is escrowed on 0G — handle it with BLINDMARKET_SETTLEMENT=0g and a local key, or from the web app.'
|
|
173
|
+
: 'The backend is building Base transactions — re-run and discovery will re-check, or set BLINDMARKET_SETTLEMENT=base.'));
|
|
142
174
|
e.code = 'ESCROW_MISMATCH';
|
|
143
175
|
throw e;
|
|
144
176
|
}
|
|
@@ -204,17 +236,17 @@ export function registerRentTools(server, cfg, walletCtx) {
|
|
|
204
236
|
e.code = 'TX_PENDING';
|
|
205
237
|
throw e;
|
|
206
238
|
}
|
|
207
|
-
/**
|
|
208
|
-
* allowance first. Confirmed by re-reading allowance()
|
|
209
|
-
* receipt, which is what makes the user-op case decidable. */
|
|
239
|
+
/** Relay chains only: createTask pulls the ERC-20 via transferFrom, so the
|
|
240
|
+
* escrow needs an allowance first. Confirmed by re-reading allowance()
|
|
241
|
+
* rather than by receipt, which is what makes the user-op case decidable. */
|
|
210
242
|
async function ensureAllowance(s, record) {
|
|
211
243
|
const need = BigInt(record.amountWei);
|
|
212
|
-
const token =
|
|
244
|
+
const token = settlementToken(s);
|
|
213
245
|
if ((await token.allowance(s.payFrom, s.escrowAddress)) >= need)
|
|
214
246
|
return;
|
|
215
247
|
const approve = async () => {
|
|
216
248
|
const data = ERC20.encodeFunctionData('approve', [s.escrowAddress, need]);
|
|
217
|
-
const { hash } = await relaySend(s, { to: s.
|
|
249
|
+
const { hash } = await relaySend(s, { to: s.token.address, data });
|
|
218
250
|
// Persist BEFORE waiting: a crash here must resume into the poll below.
|
|
219
251
|
updateSpend(record.idempotencyKey, { stage: 'approved', approveTxHash: hash });
|
|
220
252
|
record.stage = 'approved';
|
|
@@ -242,19 +274,19 @@ export function registerRentTools(server, cfg, walletCtx) {
|
|
|
242
274
|
await approve();
|
|
243
275
|
if (await settled())
|
|
244
276
|
return;
|
|
245
|
-
const e = new Error(
|
|
277
|
+
const e = new Error(`${s.symbol} allowance still below ${formatUnits(need, s.decimals)} after two approves (last ${record.approveTxHash}) — check the relay wallet's ${s.symbol} balance and retry with the same idempotencyKey`);
|
|
246
278
|
e.code = 'APPROVE_PENDING';
|
|
247
279
|
throw e;
|
|
248
280
|
}
|
|
249
281
|
/** Fund escrow + index — the shared tail of rent_service and post_task.
|
|
250
282
|
* Resumable at every stage via the spend ledger.
|
|
251
283
|
*
|
|
252
|
-
* Two funding paths, chosen by
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
* createTask the backend built,
|
|
256
|
-
* no local signing at all. The
|
|
257
|
-
* check it is the one we approved. */
|
|
284
|
+
* Two funding paths, chosen by `payment`:
|
|
285
|
+
* local-native — native 0G from the local wallet, signed and sent here.
|
|
286
|
+
* relay-erc20 — the ERC-20 (USDC on Base) via transferFrom: approve
|
|
287
|
+
* first (ensureAllowance), then the createTask the backend built,
|
|
288
|
+
* both through the Privy relay with no local signing at all. The
|
|
289
|
+
* backend picks the escrow; we only check it is the one we approved. */
|
|
258
290
|
async function fundAndIndex(record) {
|
|
259
291
|
const s = await settlement();
|
|
260
292
|
let { txHash } = record;
|
|
@@ -267,11 +299,14 @@ export function registerRentTools(server, cfg, walletCtx) {
|
|
|
267
299
|
throw e;
|
|
268
300
|
}
|
|
269
301
|
if (record.stage === 'created' || record.stage === 'approved') {
|
|
270
|
-
|
|
302
|
+
const refused = notPostingChain(s);
|
|
303
|
+
if (refused)
|
|
304
|
+
throw refused;
|
|
305
|
+
if (s.payment === 'relay-erc20')
|
|
271
306
|
await ensureAllowance(s, record);
|
|
272
|
-
const { unsignedTx } = await api('POST', '/api/v1/tasks', {
|
|
307
|
+
const { unsignedTx, chain: builtChain, chainId: builtChainId } = await api('POST', '/api/v1/tasks', {
|
|
273
308
|
taskHash: record.taskHash,
|
|
274
|
-
token: s.
|
|
309
|
+
token: s.payment === 'relay-erc20' ? s.token.address : ZERO_TOKEN,
|
|
275
310
|
amount: record.amountWei,
|
|
276
311
|
locationZone: 'global',
|
|
277
312
|
duration: String(record.durationSecs ?? 3600),
|
|
@@ -282,10 +317,19 @@ export function registerRentTools(server, cfg, walletCtx) {
|
|
|
282
317
|
rootHash: record.rootHash,
|
|
283
318
|
wrappedKeys: record.privacy === 'public' ? undefined : record.wrappedKeys,
|
|
284
319
|
});
|
|
285
|
-
// Either branch: the tx must target the escrow this mode expects. On
|
|
286
|
-
//
|
|
320
|
+
// Either branch: the tx must target the escrow this mode expects. On a
|
|
321
|
+
// relay chain that is also the escrow the allowance above was granted to.
|
|
322
|
+
// A chain-aware backend also names the chain it built for (older ones
|
|
323
|
+
// return only the tx, and are checked by address alone, as before).
|
|
324
|
+
if (s.postingChain !== undefined && ((builtChain !== undefined && builtChain !== s.mode) ||
|
|
325
|
+
(builtChainId !== undefined && s.chainId !== undefined && Number(builtChainId) !== s.chainId))) {
|
|
326
|
+
settlement.invalidate();
|
|
327
|
+
const e = new Error(`backend built createTask for ${builtChain ?? '?'} (chain ${builtChainId ?? '?'}) but this process settles on ${s.mode} (chain ${s.chainId ?? '?'}) — re-run and discovery will re-check`);
|
|
328
|
+
e.code = 'ESCROW_MISMATCH';
|
|
329
|
+
throw e;
|
|
330
|
+
}
|
|
287
331
|
await verifyTarget(s, unsignedTx.to, 'createTask');
|
|
288
|
-
if (s.
|
|
332
|
+
if (s.payment === 'relay-erc20') {
|
|
289
333
|
const { hash, isUserOp, gas } = await relaySend(s, { to: unsignedTx.to, data: unsignedTx.data });
|
|
290
334
|
// Persist BEFORE waiting, same reasoning as the 0G branch below.
|
|
291
335
|
updateSpend(record.idempotencyKey, { stage: 'funded', txHash: hash, isUserOp, gas });
|
|
@@ -300,6 +344,8 @@ export function registerRentTools(server, cfg, walletCtx) {
|
|
|
300
344
|
data: unsignedTx.data,
|
|
301
345
|
value: BigInt(record.amountWei),
|
|
302
346
|
gasLimit: GAS_LIMIT,
|
|
347
|
+
// ethers refuses to send when its provider is on another chain.
|
|
348
|
+
...(s.chainId !== undefined ? { chainId: s.chainId } : {}),
|
|
303
349
|
});
|
|
304
350
|
// Persist the tx hash BEFORE waiting: if we crash mid-confirmation the
|
|
305
351
|
// resume path re-runs /tasks/index with this hash instead of re-funding.
|
|
@@ -383,6 +429,9 @@ export function registerRentTools(server, cfg, walletCtx) {
|
|
|
383
429
|
return fail(err.code ?? 'RESUME_FAILED', err.message);
|
|
384
430
|
}
|
|
385
431
|
}
|
|
432
|
+
const offPosting = notPostingChain(s);
|
|
433
|
+
if (offPosting)
|
|
434
|
+
return fail(offPosting.code, offPosting.message);
|
|
386
435
|
const service = await api('GET', `/api/v1/marketplace/services/${serviceId}`);
|
|
387
436
|
const isPublic = privacy === 'public';
|
|
388
437
|
if (!isPublic && !service.agent_public_key) {
|
|
@@ -395,10 +444,10 @@ export function registerRentTools(server, cfg, walletCtx) {
|
|
|
395
444
|
// A service priced in 18-decimal units on a 6-decimal chain would quote
|
|
396
445
|
// as a trillion USDC and relay an approve for it before createTask
|
|
397
446
|
// failed. The backend converts such prices since migration 31, but an
|
|
398
|
-
// older backend may still serve one. 1,000,000
|
|
447
|
+
// older backend may still serve one. 1,000,000 tokens per call is far
|
|
399
448
|
// above any real listing — refuse.
|
|
400
|
-
if (s.
|
|
401
|
-
return fail('PRICE_UNITS_SUSPECT', `service ${serviceId} lists price_raw=${priceRaw} which is ${formatUnits(priceRaw,
|
|
449
|
+
if (s.decimals < 18 && priceRaw > 1000000n * 10n ** BigInt(s.decimals)) {
|
|
450
|
+
return fail('PRICE_UNITS_SUSPECT', `service ${serviceId} lists price_raw=${priceRaw} which is ${formatUnits(priceRaw, s.decimals)} ${s.symbol} — this looks like an 18-decimal 0G price on a ${s.symbol} chain. Not sending. Re-list the service in ${s.symbol} base units.`);
|
|
402
451
|
}
|
|
403
452
|
const price = formatUnits(priceRaw, s.decimals);
|
|
404
453
|
if (!confirm) {
|
|
@@ -453,11 +502,11 @@ export function registerRentTools(server, cfg, walletCtx) {
|
|
|
453
502
|
wrappedKeys,
|
|
454
503
|
publicBrief: isPublic ? prompt.slice(0, 4000) : undefined,
|
|
455
504
|
verificationMode: 'auto',
|
|
456
|
-
verificationCriteria:
|
|
505
|
+
verificationCriteria: RENTAL_VERIFICATION_CRITERIA,
|
|
457
506
|
requiredCapabilities: [],
|
|
458
507
|
amountWei: String(service.price_raw),
|
|
459
508
|
settlement: s.mode,
|
|
460
|
-
token: s.
|
|
509
|
+
token: s.payment === 'relay-erc20' ? s.token.address : ZERO_TOKEN,
|
|
461
510
|
durationSecs: 3600,
|
|
462
511
|
createdAt: new Date().toISOString(),
|
|
463
512
|
updatedAt: new Date().toISOString(),
|
|
@@ -508,6 +557,9 @@ export function registerRentTools(server, cfg, walletCtx) {
|
|
|
508
557
|
return fail(err.code ?? 'RESUME_FAILED', err.message);
|
|
509
558
|
}
|
|
510
559
|
}
|
|
560
|
+
const offPosting = notPostingChain(s);
|
|
561
|
+
if (offPosting)
|
|
562
|
+
return fail(offPosting.code, offPosting.message);
|
|
511
563
|
const isPublic = privacy === 'public';
|
|
512
564
|
const amountStr = amount ?? amount0G;
|
|
513
565
|
if (!amountStr) {
|
|
@@ -585,7 +637,7 @@ export function registerRentTools(server, cfg, walletCtx) {
|
|
|
585
637
|
requiredCapabilities: capabilities ?? [],
|
|
586
638
|
amountWei: amountWei.toString(),
|
|
587
639
|
settlement: s.mode,
|
|
588
|
-
token: s.
|
|
640
|
+
token: s.payment === 'relay-erc20' ? s.token.address : ZERO_TOKEN,
|
|
589
641
|
durationSecs: durationSeconds ?? 86400,
|
|
590
642
|
createdAt: new Date().toISOString(),
|
|
591
643
|
updatedAt: new Date().toISOString(),
|
|
@@ -633,7 +685,8 @@ export function registerRentTools(server, cfg, walletCtx) {
|
|
|
633
685
|
//
|
|
634
686
|
// Chain note: the backend resolves which chain holds the task
|
|
635
687
|
// (resolveTaskChainById) and builds the unsigned tx for it; this side only
|
|
636
|
-
// decides who signs — the local wallet on 0G, the Privy relay on
|
|
688
|
+
// decides who signs — the local wallet on 0G, the Privy relay on a relay
|
|
689
|
+
// chain (Base) — and
|
|
637
690
|
// verifies the tx targets the escrow that mode expects before sending
|
|
638
691
|
// (verifyTarget). Task state is read from the chain the mode names, not
|
|
639
692
|
// from GET /tasks/:id, which is bound to the 0G escrow.
|
|
@@ -643,32 +696,55 @@ export function registerRentTools(server, cfg, walletCtx) {
|
|
|
643
696
|
}
|
|
644
697
|
/** Resolve a task id-or-hash to its live on-chain state.
|
|
645
698
|
*
|
|
646
|
-
* GET /api/v1/tasks/:id accepts either form — but
|
|
647
|
-
* only (escrowService.getTask
|
|
648
|
-
* struct is for whatever 0G task happens to share the
|
|
649
|
-
* use it just to turn a hash into an id, then read
|
|
650
|
-
*
|
|
699
|
+
* GET /api/v1/tasks/:id accepts either form — but older backends read the
|
|
700
|
+
* 0G escrow only (escrowService.getTask was bound to the 0G contract), so
|
|
701
|
+
* on a relay chain its struct is for whatever 0G task happens to share the
|
|
702
|
+
* id. On a relay chain we use it just to turn a hash into an id, then read
|
|
703
|
+
* the struct from that chain's escrow ourselves over the read-only
|
|
704
|
+
* provider. */
|
|
651
705
|
async function loadTask(s, task) {
|
|
652
|
-
|
|
653
|
-
|
|
706
|
+
/** The backend said the task is on another chain: its id means a different task here. */
|
|
707
|
+
const onOtherChain = (taskId, chain) => {
|
|
708
|
+
const label = s.chain === 'base' ? 'Base' : s.chain;
|
|
709
|
+
const e = new Error(`task ${taskId} is a ${chain} task, and this process settles on ${label} — handle it with BLINDMARKET_SETTLEMENT=${chain}`);
|
|
710
|
+
// The code names the chain; on Base it is the TASK_NOT_ON_BASE it always was.
|
|
711
|
+
e.code = `TASK_NOT_ON_${s.chain.toUpperCase().replace(/-/g, '_')}`;
|
|
712
|
+
return e;
|
|
713
|
+
};
|
|
714
|
+
if (s.payment !== 'relay-erc20') {
|
|
715
|
+
const detail = await api('GET', `/api/v1/tasks/${encodeURIComponent(task)}`);
|
|
716
|
+
if (detail.chain && detail.chain !== s.chain)
|
|
717
|
+
throw onOtherChain(detail.taskId, detail.chain);
|
|
718
|
+
return detail;
|
|
654
719
|
}
|
|
655
|
-
//
|
|
720
|
+
// Relay chain: the escrow is the authority, and we can read it directly. Only ask
|
|
656
721
|
// the backend when we need a hash resolved to an id — that endpoint makes
|
|
657
722
|
// several Redis round trips and is the slowest thing in this path, so a
|
|
658
723
|
// numeric id must not pay for it.
|
|
659
724
|
let taskId;
|
|
725
|
+
let backendChain;
|
|
660
726
|
if (/^\d+$/.test(task)) {
|
|
661
727
|
taskId = task;
|
|
662
728
|
}
|
|
663
729
|
else {
|
|
664
730
|
const viaBackend = await api('GET', `/api/v1/tasks/${encodeURIComponent(task)}`);
|
|
665
731
|
taskId = viaBackend.taskId;
|
|
732
|
+
backendChain = viaBackend.chain;
|
|
733
|
+
// The same id can exist on this chain's escrow too (another task of the
|
|
734
|
+
// same poster): reading it would act on the wrong task.
|
|
735
|
+
if (backendChain && backendChain !== s.chain)
|
|
736
|
+
throw onOtherChain(taskId, backendChain);
|
|
666
737
|
}
|
|
667
738
|
const escrow = new Contract(s.escrowAddress, ESCROW_READ_ABI, s.provider);
|
|
668
739
|
const t = await escrow.getTask(BigInt(taskId));
|
|
669
740
|
if (String(t.agent).toLowerCase() === ZERO_TOKEN) {
|
|
670
|
-
const
|
|
671
|
-
|
|
741
|
+
const label = s.chain === 'base' ? 'Base' : s.chain;
|
|
742
|
+
const hint = s.escrowChains
|
|
743
|
+
? `it is on another chain; set BLINDMARKET_SETTLEMENT to the one that holds it (this backend has escrows on ${s.escrowChains.join(', ')})`
|
|
744
|
+
: 'it is probably a 0G task; handle it with BLINDMARKET_SETTLEMENT=0g';
|
|
745
|
+
const e = new Error(`task ${taskId} does not exist on the ${label} escrow ${s.escrowAddress} — ${hint}`);
|
|
746
|
+
// The code names the chain; on Base it is the TASK_NOT_ON_BASE it always was.
|
|
747
|
+
e.code = `TASK_NOT_ON_${s.chain.toUpperCase().replace(/-/g, '_')}`;
|
|
672
748
|
throw e;
|
|
673
749
|
}
|
|
674
750
|
return {
|
|
@@ -682,8 +758,8 @@ export function registerRentTools(server, cfg, walletCtx) {
|
|
|
682
758
|
decimals: s.decimals,
|
|
683
759
|
};
|
|
684
760
|
}
|
|
685
|
-
/** Amount formatted against the task's OWN decimals — a
|
|
686
|
-
* USDC
|
|
761
|
+
/** Amount formatted against the task's OWN decimals — a relay-chain task
|
|
762
|
+
* settles in its ERC-20 (USDC: 6), not the wallet's native 18. */
|
|
687
763
|
function refundAmount(detail) {
|
|
688
764
|
return formatUnits(BigInt(detail.amount), detail.decimals ?? 18);
|
|
689
765
|
}
|
|
@@ -704,7 +780,7 @@ export function registerRentTools(server, cfg, walletCtx) {
|
|
|
704
780
|
}
|
|
705
781
|
/** Broadcast the refund and wait for it — the shared tail of both tools.
|
|
706
782
|
* Resumable: a record past 'created' waits on the tx it already saved.
|
|
707
|
-
* Same two paths as fundAndIndex: local wallet on 0G, Privy relay on
|
|
783
|
+
* Same two paths as fundAndIndex: local wallet on 0G, Privy relay on a relay chain.
|
|
708
784
|
* The backend resolves which chain holds the task and builds the tx for
|
|
709
785
|
* it; this only decides who signs. */
|
|
710
786
|
async function sendRefund(record) {
|
|
@@ -721,10 +797,10 @@ export function registerRentTools(server, cfg, walletCtx) {
|
|
|
721
797
|
const { unsignedTx } = await api('POST', `/api/v1/tasks/${taskId}/${route}`);
|
|
722
798
|
// The backend resolves the chain that holds the task and builds for it;
|
|
723
799
|
// the tx carries no chainId. If that chain is not the one this mode
|
|
724
|
-
// broadcasts on, stop here — relaying a 0G refund onto
|
|
725
|
-
// address with no escrow and burns the gas.
|
|
800
|
+
// broadcasts on, stop here — relaying a 0G refund onto another chain
|
|
801
|
+
// lands on an address with no escrow and burns the gas.
|
|
726
802
|
await verifyTarget(s, unsignedTx.to, `${route}Task`);
|
|
727
|
-
if (s.
|
|
803
|
+
if (s.payment === 'relay-erc20') {
|
|
728
804
|
const { hash, isUserOp, gas } = await relaySend(s, { to: unsignedTx.to, data: unsignedTx.data });
|
|
729
805
|
// Persist BEFORE waiting, same reasoning as the 0G branch below.
|
|
730
806
|
updateSpend(record.idempotencyKey, { stage: 'sent', txHash: hash, isUserOp, gas });
|
|
@@ -738,6 +814,7 @@ export function registerRentTools(server, cfg, walletCtx) {
|
|
|
738
814
|
to: unsignedTx.to,
|
|
739
815
|
data: unsignedTx.data,
|
|
740
816
|
gasLimit: GAS_LIMIT,
|
|
817
|
+
...(s.chainId !== undefined ? { chainId: s.chainId } : {}),
|
|
741
818
|
});
|
|
742
819
|
// Persist the hash BEFORE waiting, same reasoning as fundAndIndex: a
|
|
743
820
|
// crash mid-confirmation must resume onto THIS tx, not broadcast another.
|
|
@@ -747,7 +824,7 @@ export function registerRentTools(server, cfg, walletCtx) {
|
|
|
747
824
|
}
|
|
748
825
|
}
|
|
749
826
|
else if (txHash) {
|
|
750
|
-
if (s.
|
|
827
|
+
if (s.payment === 'relay-erc20')
|
|
751
828
|
await waitRelayed(s, txHash, record.isUserOp ?? false);
|
|
752
829
|
else
|
|
753
830
|
await walletCtx.provider.waitForTransaction(txHash);
|
|
@@ -1007,7 +1084,7 @@ export function registerRentTools(server, cfg, walletCtx) {
|
|
|
1007
1084
|
});
|
|
1008
1085
|
server.registerTool('complete_task', {
|
|
1009
1086
|
title: 'Deliver a Task Result and Settle',
|
|
1010
|
-
description: 'Executor side, after accept_task: submits your result, sends submitEvidence from YOUR wallet (Base: through the backend relay, gas in USDC; 0G: signed locally with BLINDMARKET_PRIVATE_KEY), then asks the backend to verify and release the escrow to you. If the last verification FAILED, calling this again resubmits — the contract allows up to 3 attempts before the deadline. Safe to re-call: it resumes from whatever stage the escrow shows.',
|
|
1087
|
+
description: 'Executor side, after accept_task: submits your result, sends submitEvidence from YOUR wallet (Base: through the backend relay, gas in USDC; 0G: signed locally with BLINDMARKET_PRIVATE_KEY), then asks the backend to verify and release the escrow to you. This is the ONLY delivery tool — there is no separate submit step. If an earlier call died between submitting and broadcasting (task stuck "submitted" off-chain, Assigned on-chain), re-calling heals it via the /rebroadcast endpoint. If the last verification FAILED, calling this again resubmits — the contract allows up to 3 attempts before the deadline. Safe to re-call: it resumes from whatever stage the escrow shows.',
|
|
1011
1088
|
inputSchema: {
|
|
1012
1089
|
task: z.string().regex(/^0x[0-9a-fA-F]{64}$/).describe('The 0x task hash — A2A tasks are addressed by hash, not by numeric id'),
|
|
1013
1090
|
output: z.string().min(1).max(200_000).describe('Your result. Verification judges this text (auto mode scores it against the poster\'s criteria).'),
|
|
@@ -1019,7 +1096,7 @@ export function registerRentTools(server, cfg, walletCtx) {
|
|
|
1019
1096
|
return f.error;
|
|
1020
1097
|
const { s, payFrom } = f;
|
|
1021
1098
|
// requireFunding returns an error on 0G without a wallet, so walletCtx is
|
|
1022
|
-
// non-null whenever s.
|
|
1099
|
+
// non-null whenever s.payment is 'local-native' from here on.
|
|
1023
1100
|
let detail;
|
|
1024
1101
|
try {
|
|
1025
1102
|
detail = await loadTask(s, task);
|
|
@@ -1055,12 +1132,27 @@ export function registerRentTools(server, cfg, walletCtx) {
|
|
|
1055
1132
|
}
|
|
1056
1133
|
let submitTxHash;
|
|
1057
1134
|
let gas;
|
|
1135
|
+
let healed = false;
|
|
1058
1136
|
try {
|
|
1059
1137
|
if (status === 1 || isRetry) {
|
|
1060
|
-
|
|
1138
|
+
let sub;
|
|
1139
|
+
try {
|
|
1140
|
+
sub = await api('POST', `/api/v1/a2a/tasks/${task}/submit`, { resultData: { output } });
|
|
1141
|
+
}
|
|
1142
|
+
catch (err) {
|
|
1143
|
+
// Stranded 'submitted': /submit flips the off-chain state when the
|
|
1144
|
+
// tx is BUILT, so an earlier call that died before broadcasting
|
|
1145
|
+
// leaves the chain at Assigned(1) while /submit refuses a rebuild.
|
|
1146
|
+
// /rebroadcast rebuilds the same tx from the stored result — the
|
|
1147
|
+
// FIRST output is what gets delivered, not this call's.
|
|
1148
|
+
if (err.code !== 'INVALID_STATE' || status !== 1)
|
|
1149
|
+
throw err;
|
|
1150
|
+
sub = await api('POST', `/api/v1/a2a/tasks/${task}/rebroadcast`);
|
|
1151
|
+
healed = true;
|
|
1152
|
+
}
|
|
1061
1153
|
const tx = sub.unsignedSubmitEvidence;
|
|
1062
1154
|
await verifyTarget(s, tx.to, 'submitEvidence');
|
|
1063
|
-
if (s.
|
|
1155
|
+
if (s.payment === 'relay-erc20') {
|
|
1064
1156
|
const sent = await relaySend(s, { to: tx.to, data: tx.data });
|
|
1065
1157
|
submitTxHash = sent.hash;
|
|
1066
1158
|
gas = sent.gas;
|
|
@@ -1098,13 +1190,14 @@ export function registerRentTools(server, cfg, walletCtx) {
|
|
|
1098
1190
|
taskHash: detail.taskHash,
|
|
1099
1191
|
submitTxHash,
|
|
1100
1192
|
gas,
|
|
1193
|
+
...(healed ? { rebroadcast: true, note: 'An earlier submission for this task never reached the chain; its stored result was re-broadcast. The output passed to THIS call was not used.' } : {}),
|
|
1101
1194
|
verification: fin.verificationResult ?? null,
|
|
1102
1195
|
backendStatus: fin.status,
|
|
1103
1196
|
onChainStatus: statusName(Number(after.status)),
|
|
1104
1197
|
paidTo: done ? payFrom : undefined,
|
|
1105
1198
|
submissionAttempts: after.submissionAttempts,
|
|
1106
1199
|
hint: done
|
|
1107
|
-
? `Escrow released: ${formatUnits(BigInt(after.amount), after.decimals ??
|
|
1200
|
+
? `Escrow released: ${formatUnits(BigInt(after.amount), after.decimals ?? s.decimals)} ${s.symbol} minus the marketplace fee is now in ${payFrom}.`
|
|
1108
1201
|
: fin.awaitingPosterApproval
|
|
1109
1202
|
? 'Manual-verification task: the poster must approve via verify_task before the escrow releases.'
|
|
1110
1203
|
: `Verification did not pass (${(fin.verificationResult?.reasons ?? []).join('; ') || 'no reasons given'}). ` +
|