@lucid-agents/taskmarket 1.17.0 → 1.17.2
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/agent-compatibility.json +1 -1
- package/dist/budget-view-XKZC3GY5.js +36 -0
- package/dist/channels-JF4Q5KG3.js +29 -0
- package/dist/chunk-52Q2DCZN.js +1099 -0
- package/dist/chunk-73RXOYME.js +185 -0
- package/dist/{chunk-3XPF2O6L.js → chunk-FRV3ZHUR.js} +5381 -6300
- package/dist/chunk-GJQUALXN.js +1062 -0
- package/dist/chunk-LDCFKFF6.js +154 -0
- package/dist/{compatibility-F3RC2GX7.js → chunk-OKZ2NRYZ.js} +5 -5
- package/dist/chunk-XXD7TWLP.js +181 -0
- package/dist/{chunk-O27J7ZPH.js → chunk-YM4S6IE4.js} +2 -1
- package/dist/compatibility-KUYGZSUC.js +19 -0
- package/dist/doctor-7TWLIJWW.js +216 -0
- package/dist/{errors-F3EGNA4F.js → errors-2L357HGA.js} +1 -1
- package/dist/index.js +317 -162
- package/dist/{launch-EZJGJJ5Y.js → launch-2U3KFTFD.js} +1414 -700
- package/dist/pi-extensions/remote-tools.cjs +103 -0
- package/dist/pi-extensions/xgate.cjs +310 -5
- package/dist/reconcile-XZR2UYSW.js +83 -0
- package/dist/status-5IMULBQQ.js +18 -0
- package/package.json +3 -3
|
@@ -0,0 +1,1099 @@
|
|
|
1
|
+
import { createRequire as __tmCreateRequire } from 'node:module'; import { fileURLToPath as __tmFileURLToPath } from 'node:url'; import { dirname as __tmDirname } from 'node:path'; const require = __tmCreateRequire(import.meta.url); const __tmFilename = __tmFileURLToPath(import.meta.url); const __tmDirnameValue = __tmDirname(__tmFilename);
|
|
2
|
+
import {
|
|
3
|
+
ApiError,
|
|
4
|
+
ComputeCreditBuyOutputSchema,
|
|
5
|
+
ComputeCreditPricingSchema,
|
|
6
|
+
ComputeCreditStatusOutputSchema,
|
|
7
|
+
X402PayError,
|
|
8
|
+
apiErrorEnvelopeOf,
|
|
9
|
+
apiGet,
|
|
10
|
+
apiOrigin,
|
|
11
|
+
apiPost,
|
|
12
|
+
apiUrl,
|
|
13
|
+
assertX402HeaderName,
|
|
14
|
+
assertX402HeaderValue,
|
|
15
|
+
createEvmPerRequestRail,
|
|
16
|
+
createMppTempoCredential,
|
|
17
|
+
createTaskmarketFeeRail,
|
|
18
|
+
createWalletAccountFromKeystore,
|
|
19
|
+
createX402Allocations,
|
|
20
|
+
createX402Payer,
|
|
21
|
+
failureMessage,
|
|
22
|
+
getTaskmarketHome,
|
|
23
|
+
getX402JournalPath,
|
|
24
|
+
getX402Payment,
|
|
25
|
+
getX402PolicyPath,
|
|
26
|
+
idempotencyHeaders,
|
|
27
|
+
legalReceiptHeadersForKeystore,
|
|
28
|
+
loadKeystore,
|
|
29
|
+
loadX402Policy,
|
|
30
|
+
parseFailureBody,
|
|
31
|
+
paymentProtocolOf,
|
|
32
|
+
readFailureBody,
|
|
33
|
+
readMppTempoChallenge,
|
|
34
|
+
reconcileX402Payment,
|
|
35
|
+
resolveIdempotencyKey,
|
|
36
|
+
selectedNetwork,
|
|
37
|
+
withIdempotentWrite,
|
|
38
|
+
withX402FileLock
|
|
39
|
+
} from "./chunk-FRV3ZHUR.js";
|
|
40
|
+
|
|
41
|
+
// src/lib/compute-credits.ts
|
|
42
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
43
|
+
import { promises as fs2 } from "fs";
|
|
44
|
+
import path2 from "path";
|
|
45
|
+
|
|
46
|
+
// src/lib/x402-fee.ts
|
|
47
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
48
|
+
|
|
49
|
+
// src/lib/paid-write.ts
|
|
50
|
+
function paidWriteRequest(keystore, path3, body, idempotencyKey) {
|
|
51
|
+
return {
|
|
52
|
+
url: `${apiUrl()}${path3}`,
|
|
53
|
+
headers: {
|
|
54
|
+
"Content-Type": "application/json",
|
|
55
|
+
...legalReceiptHeadersForKeystore(keystore, path3, "POST"),
|
|
56
|
+
...idempotencyHeaders(idempotencyKey)
|
|
57
|
+
},
|
|
58
|
+
body: JSON.stringify(body)
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function refusedPaidWrite(path3, status, read, idempotencyKey) {
|
|
62
|
+
return new ApiError(
|
|
63
|
+
status,
|
|
64
|
+
`POST ${path3} failed (${status}): ${read.text}`,
|
|
65
|
+
idempotencyKey,
|
|
66
|
+
apiErrorEnvelopeOf(read.body) ?? void 0
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
function failedAfterPayment(path3, status, read, idempotencyKey) {
|
|
70
|
+
return new ApiError(
|
|
71
|
+
status,
|
|
72
|
+
failureMessage(`POST ${path3} failed after payment`, status, read),
|
|
73
|
+
idempotencyKey,
|
|
74
|
+
apiErrorEnvelopeOf(read.body) ?? void 0
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// src/lib/mpp.ts
|
|
79
|
+
var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([502, 503, 504]);
|
|
80
|
+
function inFlight(path3, read, key) {
|
|
81
|
+
const envelope = apiErrorEnvelopeOf(read.body) ?? void 0;
|
|
82
|
+
const txHash = read.body?.taskmarket?.txHash;
|
|
83
|
+
return new ApiError(
|
|
84
|
+
409,
|
|
85
|
+
`POST ${path3}: the payment transfer${typeof txHash === "string" ? ` ${txHash}` : ""} was submitted but its outcome is not known yet. Do not pay again; re-run with TASKMARKET_IDEMPOTENCY_KEY=${key} to check this write.`,
|
|
86
|
+
key,
|
|
87
|
+
envelope
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
async function mppPost(path3, body, options = {}) {
|
|
91
|
+
const keystore = await loadKeystore();
|
|
92
|
+
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
93
|
+
const idempotencyKey = resolveIdempotencyKey(options.idempotencyKey);
|
|
94
|
+
const chainId = options.chainId ?? selectedNetwork().chainId;
|
|
95
|
+
const request = paidWriteRequest(keystore, path3, { chainId, ...body }, idempotencyKey);
|
|
96
|
+
const send = (headers) => fetchImpl(request.url, {
|
|
97
|
+
method: "POST",
|
|
98
|
+
redirect: "error",
|
|
99
|
+
headers: { ...request.headers, ...headers },
|
|
100
|
+
body: request.body
|
|
101
|
+
});
|
|
102
|
+
return withIdempotentWrite(idempotencyKey, async () => {
|
|
103
|
+
const r1 = await send({});
|
|
104
|
+
if (r1.status !== 402) {
|
|
105
|
+
if (r1.ok) return { data: await r1.json(), idempotencyKey };
|
|
106
|
+
throw refusedPaidWrite(path3, r1.status, await readFailureBody(r1), idempotencyKey);
|
|
107
|
+
}
|
|
108
|
+
const challenge = readMppTempoChallenge(r1.headers);
|
|
109
|
+
if (!challenge) throw new Error("No payment methods accepted by server");
|
|
110
|
+
const account = options.account ?? await createWalletAccountFromKeystore(keystore);
|
|
111
|
+
const credential = await createMppTempoCredential({
|
|
112
|
+
challenge,
|
|
113
|
+
signer: { address: account.address, sign: (args) => account.sign(args) },
|
|
114
|
+
chainId,
|
|
115
|
+
nowSeconds: options.nowSeconds?.(),
|
|
116
|
+
expect: options.expect && {
|
|
117
|
+
amount: options.expect.amount,
|
|
118
|
+
recipient: options.expect.payTo
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
const now = options.nowSeconds ?? (() => Math.floor(Date.now() / 1e3));
|
|
122
|
+
const maxRetries = options.maxPaidRetries ?? 2;
|
|
123
|
+
for (let attempt = 0; ; attempt++) {
|
|
124
|
+
const canRetry = attempt < maxRetries && now() < credential.validBefore;
|
|
125
|
+
let r2;
|
|
126
|
+
try {
|
|
127
|
+
r2 = await send({ Authorization: credential.authorization });
|
|
128
|
+
} catch (error) {
|
|
129
|
+
if (canRetry) continue;
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
if (r2.ok) return { data: await r2.json(), idempotencyKey };
|
|
133
|
+
const read = await readFailureBody(r2);
|
|
134
|
+
if (RETRYABLE_STATUSES.has(r2.status) && canRetry) continue;
|
|
135
|
+
const reason = apiErrorEnvelopeOf(read.body)?.reason;
|
|
136
|
+
if (r2.status === 409 && reason === "intent_in_flight")
|
|
137
|
+
throw inFlight(path3, read, idempotencyKey);
|
|
138
|
+
if (r2.status === 402) {
|
|
139
|
+
throw refusedPaidWrite(path3, r2.status, read, idempotencyKey);
|
|
140
|
+
}
|
|
141
|
+
throw failedAfterPayment(path3, r2.status, read, idempotencyKey);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// src/lib/x402-pay-client.ts
|
|
147
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
148
|
+
|
|
149
|
+
// src/lib/x402-http.ts
|
|
150
|
+
import { createHash, randomUUID } from "crypto";
|
|
151
|
+
import { promises as fs } from "fs";
|
|
152
|
+
import path from "path";
|
|
153
|
+
async function loadExternalHeaders(input) {
|
|
154
|
+
const result = {};
|
|
155
|
+
if (input.headersFile) {
|
|
156
|
+
const handle = await fs.open(input.headersFile, "r");
|
|
157
|
+
let raw;
|
|
158
|
+
try {
|
|
159
|
+
const stat = await handle.stat();
|
|
160
|
+
if (!stat.isFile())
|
|
161
|
+
throw new Error(`Headers file must be a regular file: ${input.headersFile}`);
|
|
162
|
+
if ((stat.mode & 63) !== 0) {
|
|
163
|
+
throw new Error(
|
|
164
|
+
`Headers file must not be accessible by group or others: ${input.headersFile}`
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
raw = await handle.readFile("utf8");
|
|
168
|
+
} finally {
|
|
169
|
+
await handle.close();
|
|
170
|
+
}
|
|
171
|
+
const parsed = JSON.parse(raw);
|
|
172
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
173
|
+
throw new Error("Headers file must contain a JSON object");
|
|
174
|
+
}
|
|
175
|
+
for (const [name, value] of Object.entries(parsed)) {
|
|
176
|
+
if (typeof value !== "string") throw new Error(`Header '${name}' must have a string value`);
|
|
177
|
+
result[assertX402HeaderName(name)] = assertX402HeaderValue(value);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
for (const mapping of input.headerEnv ?? []) {
|
|
181
|
+
const separator = mapping.indexOf("=");
|
|
182
|
+
if (separator <= 0 || separator === mapping.length - 1) {
|
|
183
|
+
throw new Error(`--header-env must use HEADER=ENV_VAR: ${mapping}`);
|
|
184
|
+
}
|
|
185
|
+
const name = assertX402HeaderName(mapping.slice(0, separator));
|
|
186
|
+
const environmentVariable = mapping.slice(separator + 1);
|
|
187
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(environmentVariable)) {
|
|
188
|
+
throw new Error(`Invalid environment variable name: ${environmentVariable}`);
|
|
189
|
+
}
|
|
190
|
+
const value = process.env[environmentVariable];
|
|
191
|
+
if (value === void 0)
|
|
192
|
+
throw new Error(`Environment variable ${environmentVariable} is not set`);
|
|
193
|
+
if (result[name] !== void 0)
|
|
194
|
+
throw new Error(`HTTP header '${name}' was provided more than once`);
|
|
195
|
+
result[name] = assertX402HeaderValue(value);
|
|
196
|
+
}
|
|
197
|
+
return result;
|
|
198
|
+
}
|
|
199
|
+
function requestBodyTooLarge(maxBytes) {
|
|
200
|
+
return new X402PayError(`Request body exceeds ${maxBytes} bytes`, {
|
|
201
|
+
code: "request_body_too_large"
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
async function readBoundedBodyFile(bodyFile, maxBytes) {
|
|
205
|
+
const handle = await fs.open(bodyFile, "r");
|
|
206
|
+
try {
|
|
207
|
+
const stat = await handle.stat();
|
|
208
|
+
if (stat.isFile() && stat.size > maxBytes) throw requestBodyTooLarge(maxBytes);
|
|
209
|
+
const chunks = [];
|
|
210
|
+
let total = 0;
|
|
211
|
+
for (; ; ) {
|
|
212
|
+
const chunk = Buffer.alloc(Math.min(64 * 1024, maxBytes + 1 - total));
|
|
213
|
+
const { bytesRead } = await handle.read(chunk, 0, chunk.length, null);
|
|
214
|
+
if (bytesRead === 0) break;
|
|
215
|
+
total += bytesRead;
|
|
216
|
+
if (total > maxBytes) throw requestBodyTooLarge(maxBytes);
|
|
217
|
+
chunks.push(chunk.subarray(0, bytesRead));
|
|
218
|
+
}
|
|
219
|
+
return Buffer.concat(chunks, total).toString("utf8");
|
|
220
|
+
} finally {
|
|
221
|
+
await handle.close();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
async function readX402RequestBody(input) {
|
|
225
|
+
const method = input.method.toUpperCase();
|
|
226
|
+
if (method !== "GET" && method !== "POST") throw new Error("x402 method must be GET or POST");
|
|
227
|
+
if (input.json !== void 0 && input.bodyFile) {
|
|
228
|
+
throw new Error("Use only one of --json or --body-file");
|
|
229
|
+
}
|
|
230
|
+
if (method === "GET" && (input.json !== void 0 || input.bodyFile)) {
|
|
231
|
+
throw new Error("GET x402 requests cannot include a body");
|
|
232
|
+
}
|
|
233
|
+
if (method === "POST" && input.json === void 0 && !input.bodyFile) {
|
|
234
|
+
throw new Error("POST x402 requests require --json or --body-file");
|
|
235
|
+
}
|
|
236
|
+
if (method !== "POST") return void 0;
|
|
237
|
+
const maxBytes = input.maxBytes ?? Number.MAX_SAFE_INTEGER;
|
|
238
|
+
let raw;
|
|
239
|
+
if (input.bodyFile) {
|
|
240
|
+
raw = await readBoundedBodyFile(input.bodyFile, maxBytes);
|
|
241
|
+
} else {
|
|
242
|
+
raw = input.json;
|
|
243
|
+
if (Buffer.byteLength(raw, "utf8") > maxBytes) throw requestBodyTooLarge(maxBytes);
|
|
244
|
+
}
|
|
245
|
+
try {
|
|
246
|
+
JSON.parse(raw);
|
|
247
|
+
} catch {
|
|
248
|
+
throw new Error("POST body is not valid JSON");
|
|
249
|
+
}
|
|
250
|
+
return Buffer.from(raw, "utf8");
|
|
251
|
+
}
|
|
252
|
+
async function writeExclusiveAtomic(outputPath, bytes) {
|
|
253
|
+
const directory = path.dirname(outputPath);
|
|
254
|
+
await fs.mkdir(directory, { recursive: true });
|
|
255
|
+
const temporaryPath = path.join(directory, `.${path.basename(outputPath)}-${randomUUID()}.tmp`);
|
|
256
|
+
try {
|
|
257
|
+
await fs.writeFile(temporaryPath, bytes, { flag: "wx", mode: 384 });
|
|
258
|
+
await fs.link(temporaryPath, outputPath);
|
|
259
|
+
await fs.unlink(temporaryPath);
|
|
260
|
+
} catch (error) {
|
|
261
|
+
if (error.code === "EEXIST") {
|
|
262
|
+
throw new Error(`Output file already exists: ${outputPath}`);
|
|
263
|
+
}
|
|
264
|
+
throw error;
|
|
265
|
+
} finally {
|
|
266
|
+
await fs.rm(temporaryPath, { force: true }).catch(() => void 0);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
async function externalResponseData(response, options = {}) {
|
|
270
|
+
const bytes = response instanceof Response ? new Uint8Array(await response.arrayBuffer()) : response.body;
|
|
271
|
+
const contentType = response instanceof Response ? response.headers.get("content-type") ?? "application/octet-stream" : response.contentType;
|
|
272
|
+
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
|
273
|
+
if (options.outputPath) {
|
|
274
|
+
await writeExclusiveAtomic(options.outputPath, bytes);
|
|
275
|
+
return {
|
|
276
|
+
status: response.status,
|
|
277
|
+
contentType,
|
|
278
|
+
bytes: bytes.length,
|
|
279
|
+
sha256,
|
|
280
|
+
outputPath: path.resolve(options.outputPath)
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
const inlineLimit = options.inlineLimitBytes ?? 1048576;
|
|
284
|
+
if (bytes.length > inlineLimit) {
|
|
285
|
+
throw new Error(
|
|
286
|
+
`Response is ${bytes.length} bytes; use --output for responses over ${inlineLimit}`
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
const mediaType = contentType.split(";", 1)[0].trim().toLowerCase();
|
|
290
|
+
const text = Buffer.from(bytes).toString("utf8");
|
|
291
|
+
if (mediaType === "application/json" || mediaType.endsWith("+json")) {
|
|
292
|
+
let body;
|
|
293
|
+
try {
|
|
294
|
+
body = JSON.parse(text);
|
|
295
|
+
} catch {
|
|
296
|
+
throw new Error("Response declared JSON but contained invalid JSON");
|
|
297
|
+
}
|
|
298
|
+
return { status: response.status, contentType, bytes: bytes.length, sha256, body };
|
|
299
|
+
}
|
|
300
|
+
if (mediaType.startsWith("text/")) {
|
|
301
|
+
return {
|
|
302
|
+
status: response.status,
|
|
303
|
+
contentType,
|
|
304
|
+
bytes: bytes.length,
|
|
305
|
+
sha256,
|
|
306
|
+
body: text,
|
|
307
|
+
bodyEncoding: "utf8"
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
return {
|
|
311
|
+
status: response.status,
|
|
312
|
+
contentType,
|
|
313
|
+
bytes: bytes.length,
|
|
314
|
+
sha256,
|
|
315
|
+
body: Buffer.from(bytes).toString("base64"),
|
|
316
|
+
bodyEncoding: "base64"
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// src/lib/x402-pay-client.ts
|
|
321
|
+
var X402_CLI_BODY_LIMIT_BYTES = 64 * 1024 * 1024;
|
|
322
|
+
function cliX402Rails() {
|
|
323
|
+
return [createEvmPerRequestRail(), createTaskmarketFeeRail()];
|
|
324
|
+
}
|
|
325
|
+
function createCliX402Payer(options = {}) {
|
|
326
|
+
const policyPath = getX402PolicyPath();
|
|
327
|
+
return createX402Payer({
|
|
328
|
+
journalPath: getX402JournalPath(),
|
|
329
|
+
loadPolicy: (loadOptions) => loadX402Policy(policyPath, loadOptions),
|
|
330
|
+
getAccount: async () => options.account ?? await createWalletAccountFromKeystore(await loadKeystore()),
|
|
331
|
+
rails: cliX402Rails(),
|
|
332
|
+
fetch: options.fetchImpl,
|
|
333
|
+
limits: {
|
|
334
|
+
maxRequestBodyBytes: X402_CLI_BODY_LIMIT_BYTES,
|
|
335
|
+
maxResponseBodyBytes: X402_CLI_BODY_LIMIT_BYTES
|
|
336
|
+
},
|
|
337
|
+
...options.firstParty ? { firstParty: { origin: apiOrigin() } } : {}
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
async function executeX402PayRequest(options) {
|
|
341
|
+
const method = (options.method ?? "GET").toUpperCase();
|
|
342
|
+
const body = await readX402RequestBody({
|
|
343
|
+
method,
|
|
344
|
+
json: options.json,
|
|
345
|
+
bodyFile: options.bodyFile,
|
|
346
|
+
maxBytes: X402_CLI_BODY_LIMIT_BYTES
|
|
347
|
+
});
|
|
348
|
+
const payer = createCliX402Payer({ account: options.account, fetchImpl: options.fetchImpl });
|
|
349
|
+
const result = await payer.request(
|
|
350
|
+
{
|
|
351
|
+
apiVersion: 1,
|
|
352
|
+
invocationId: options.invocationId ?? randomUUID2(),
|
|
353
|
+
url: options.rawUrl,
|
|
354
|
+
method,
|
|
355
|
+
headers: options.headers,
|
|
356
|
+
body,
|
|
357
|
+
policyRuleId: options.policyRuleId,
|
|
358
|
+
nonInteractive: Boolean(options.nonInteractive)
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
confirmPayment: options.confirmPayment,
|
|
362
|
+
confirmDirectApproval: options.confirmDirectApproval
|
|
363
|
+
}
|
|
364
|
+
);
|
|
365
|
+
const payment = result.payment ? await getX402Payment(result.payment.paymentId, getX402JournalPath()) : void 0;
|
|
366
|
+
let responseData;
|
|
367
|
+
try {
|
|
368
|
+
responseData = await externalResponseData(result.response, {
|
|
369
|
+
outputPath: options.outputPath
|
|
370
|
+
});
|
|
371
|
+
} catch (error) {
|
|
372
|
+
if (payment) {
|
|
373
|
+
throw new X402PayError(
|
|
374
|
+
error instanceof Error ? error.message : "Failed to process external response",
|
|
375
|
+
{
|
|
376
|
+
code: "request_failed",
|
|
377
|
+
cause: error,
|
|
378
|
+
invocationId: result.invocationId,
|
|
379
|
+
payment,
|
|
380
|
+
pending: false,
|
|
381
|
+
status: result.response.status
|
|
382
|
+
}
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
throw error;
|
|
386
|
+
}
|
|
387
|
+
if (result.response.status < 200 || result.response.status > 299) {
|
|
388
|
+
throw new X402PayError(`External service returned HTTP ${result.response.status}`, {
|
|
389
|
+
code: "request_failed",
|
|
390
|
+
invocationId: result.invocationId,
|
|
391
|
+
payment,
|
|
392
|
+
pending: false,
|
|
393
|
+
status: result.response.status
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
return { payment, response: responseData };
|
|
397
|
+
}
|
|
398
|
+
async function reconcileX402PaymentById(paymentId) {
|
|
399
|
+
const policyPath = getX402PolicyPath();
|
|
400
|
+
return reconcileX402Payment({
|
|
401
|
+
journalPath: getX402JournalPath(),
|
|
402
|
+
paymentId,
|
|
403
|
+
loadPolicy: () => loadX402Policy(policyPath),
|
|
404
|
+
rails: cliX402Rails()
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// src/lib/x402-fee.ts
|
|
409
|
+
function bodyOf(response) {
|
|
410
|
+
return parseFailureBody(Buffer.from(response.body).toString("utf8"));
|
|
411
|
+
}
|
|
412
|
+
function isSuccess(status) {
|
|
413
|
+
return status >= 200 && status <= 299;
|
|
414
|
+
}
|
|
415
|
+
function successData(response) {
|
|
416
|
+
return JSON.parse(Buffer.from(response.body).toString("utf8"));
|
|
417
|
+
}
|
|
418
|
+
function prePaymentFailure(error) {
|
|
419
|
+
const sent = error.payment !== void 0 && error.payment.state !== "failed_before_dispatch";
|
|
420
|
+
if (error.pending || sent) return error;
|
|
421
|
+
if (error.code === "challenge_invalid" || error.code === "no_supported_rail") {
|
|
422
|
+
return new Error("No payment methods accepted by server");
|
|
423
|
+
}
|
|
424
|
+
if ((error.code === "request_failed" || error.code === "signer_unavailable") && error.cause !== void 0) {
|
|
425
|
+
return error.cause;
|
|
426
|
+
}
|
|
427
|
+
return error;
|
|
428
|
+
}
|
|
429
|
+
async function x402FeePost(path3, body, options = {}) {
|
|
430
|
+
if (paymentProtocolOf(selectedNetwork().chainId) === "mpp") {
|
|
431
|
+
if (options.allocationId !== void 0)
|
|
432
|
+
throw new Error("x402 allocations cannot pay for writes on a Tempo network");
|
|
433
|
+
return mppPost(path3, body, {
|
|
434
|
+
idempotencyKey: options.idempotencyKey,
|
|
435
|
+
expect: options.expect,
|
|
436
|
+
account: options.account,
|
|
437
|
+
fetchImpl: options.fetchImpl
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
const keystore = await loadKeystore();
|
|
441
|
+
const idempotencyKey = resolveIdempotencyKey(options.idempotencyKey);
|
|
442
|
+
const request = paidWriteRequest(keystore, path3, body, idempotencyKey);
|
|
443
|
+
return withIdempotentWrite(idempotencyKey, async () => {
|
|
444
|
+
const payer = createCliX402Payer({
|
|
445
|
+
firstParty: true,
|
|
446
|
+
account: options.account,
|
|
447
|
+
fetchImpl: options.fetchImpl
|
|
448
|
+
});
|
|
449
|
+
let result;
|
|
450
|
+
try {
|
|
451
|
+
result = await payer.request({
|
|
452
|
+
apiVersion: 1,
|
|
453
|
+
// A fresh invocation per run: the backend recognises a repeated write by its idempotency
|
|
454
|
+
// key, so a re-run must reach it rather than be answered from the local journal.
|
|
455
|
+
invocationId: options.invocationId ?? randomUUID3(),
|
|
456
|
+
url: request.url,
|
|
457
|
+
method: "POST",
|
|
458
|
+
headers: request.headers,
|
|
459
|
+
body: new TextEncoder().encode(request.body),
|
|
460
|
+
nonInteractive: true,
|
|
461
|
+
...options.allocationId !== void 0 ? { allocationId: options.allocationId } : {},
|
|
462
|
+
...options.feeKind !== void 0 ? { feeKind: options.feeKind } : {},
|
|
463
|
+
...options.expect !== void 0 ? { expect: options.expect } : {}
|
|
464
|
+
});
|
|
465
|
+
} catch (error) {
|
|
466
|
+
if (!(error instanceof X402PayError)) throw error;
|
|
467
|
+
if (error.response !== void 0) {
|
|
468
|
+
if (isSuccess(error.response.status)) {
|
|
469
|
+
return { data: successData(error.response), idempotencyKey };
|
|
470
|
+
}
|
|
471
|
+
throw failedAfterPayment(
|
|
472
|
+
path3,
|
|
473
|
+
error.response.status,
|
|
474
|
+
bodyOf(error.response),
|
|
475
|
+
idempotencyKey
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
throw prePaymentFailure(error);
|
|
479
|
+
}
|
|
480
|
+
const { response } = result;
|
|
481
|
+
if (isSuccess(response.status)) {
|
|
482
|
+
return { data: successData(response), idempotencyKey };
|
|
483
|
+
}
|
|
484
|
+
if (result.payment !== void 0) {
|
|
485
|
+
throw failedAfterPayment(path3, response.status, bodyOf(response), idempotencyKey);
|
|
486
|
+
}
|
|
487
|
+
throw refusedPaidWrite(path3, response.status, bodyOf(response), idempotencyKey);
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// src/lib/compute-credits.ts
|
|
492
|
+
var COMPUTE_CREDIT_PATH = "/api/compute/credits";
|
|
493
|
+
var COMPUTE_CREDIT_STATUS_PATH = "/api/compute/credits/status";
|
|
494
|
+
var COMPUTE_PRICING_PATH = "/api/compute/pricing";
|
|
495
|
+
var USDC_DECIMALS = 6;
|
|
496
|
+
var CONFIG_VERSION = 1;
|
|
497
|
+
var COMPUTE_PURCHASE_ARRIVAL_WINDOW_MS = 15 * 60 * 1e3;
|
|
498
|
+
var COMPUTE_CREDIT_LOCK_WAIT_MS = 2 * 60 * 1e3;
|
|
499
|
+
var COMPUTE_AUTO_TOP_UP_MAX_CHUNKS_PER_DAY = 10;
|
|
500
|
+
var ComputeCreditError = class extends Error {
|
|
501
|
+
constructor(code, message) {
|
|
502
|
+
super(message);
|
|
503
|
+
this.code = code;
|
|
504
|
+
this.name = "ComputeCreditError";
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
function getComputeCreditConfigPath() {
|
|
508
|
+
return path2.join(getTaskmarketHome(), "compute-credits.json");
|
|
509
|
+
}
|
|
510
|
+
function pendingDirectory() {
|
|
511
|
+
return path2.join(getTaskmarketHome(), "compute-credit-purchases");
|
|
512
|
+
}
|
|
513
|
+
function pendingPath(idempotencyKey) {
|
|
514
|
+
return path2.join(pendingDirectory(), `${idempotencyKey}.json`);
|
|
515
|
+
}
|
|
516
|
+
async function withComputeCreditLock(fn) {
|
|
517
|
+
const timeoutMessage = `compute credit lock: waited ${COMPUTE_CREDIT_LOCK_WAIT_MS} ms`;
|
|
518
|
+
let entered = false;
|
|
519
|
+
try {
|
|
520
|
+
return await withX402FileLock(
|
|
521
|
+
path2.join(getTaskmarketHome(), "compute-credits.lock"),
|
|
522
|
+
() => {
|
|
523
|
+
entered = true;
|
|
524
|
+
return fn();
|
|
525
|
+
},
|
|
526
|
+
{ waitMs: COMPUTE_CREDIT_LOCK_WAIT_MS, timeoutMessage }
|
|
527
|
+
);
|
|
528
|
+
} catch (error) {
|
|
529
|
+
if (entered || !(error instanceof Error) || error.message !== timeoutMessage) throw error;
|
|
530
|
+
throw new ComputeCreditError(
|
|
531
|
+
"busy",
|
|
532
|
+
"Another compute credit command on this device is still running; try again shortly"
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
async function writePrivateJson(file, value) {
|
|
537
|
+
await fs2.mkdir(path2.dirname(file), { recursive: true, mode: 448 });
|
|
538
|
+
const temporary = `${file}.${process.pid}.${randomUUID4()}.tmp`;
|
|
539
|
+
await fs2.writeFile(temporary, `${JSON.stringify(value, null, 2)}
|
|
540
|
+
`, { mode: 384 });
|
|
541
|
+
await fs2.rename(temporary, file);
|
|
542
|
+
}
|
|
543
|
+
async function loadComputeCreditConfig() {
|
|
544
|
+
let text;
|
|
545
|
+
try {
|
|
546
|
+
text = await fs2.readFile(getComputeCreditConfigPath(), "utf8");
|
|
547
|
+
} catch (error) {
|
|
548
|
+
if (error.code === "ENOENT") return void 0;
|
|
549
|
+
throw error;
|
|
550
|
+
}
|
|
551
|
+
const config = JSON.parse(text);
|
|
552
|
+
if (config.version !== CONFIG_VERSION || typeof config.allocationId !== "string") {
|
|
553
|
+
throw new ComputeCreditError(
|
|
554
|
+
"not_set_up",
|
|
555
|
+
`${getComputeCreditConfigPath()} is not a compute credit configuration this CLI can read`
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
return config;
|
|
559
|
+
}
|
|
560
|
+
async function listPendingComputePurchases() {
|
|
561
|
+
let names;
|
|
562
|
+
try {
|
|
563
|
+
names = await fs2.readdir(pendingDirectory());
|
|
564
|
+
} catch (error) {
|
|
565
|
+
if (error.code === "ENOENT") return [];
|
|
566
|
+
throw error;
|
|
567
|
+
}
|
|
568
|
+
const records = [];
|
|
569
|
+
for (const name of names.filter((entry) => entry.endsWith(".json")).sort()) {
|
|
570
|
+
let text;
|
|
571
|
+
try {
|
|
572
|
+
text = await fs2.readFile(path2.join(pendingDirectory(), name), "utf8");
|
|
573
|
+
} catch (error) {
|
|
574
|
+
if (error.code === "ENOENT") continue;
|
|
575
|
+
throw error;
|
|
576
|
+
}
|
|
577
|
+
records.push(JSON.parse(text));
|
|
578
|
+
}
|
|
579
|
+
return records.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
580
|
+
}
|
|
581
|
+
async function forgetPending(idempotencyKey) {
|
|
582
|
+
await fs2.rm(pendingPath(idempotencyKey), { force: true });
|
|
583
|
+
}
|
|
584
|
+
function usdToAtomic(value) {
|
|
585
|
+
const match = /^(\d{1,9})(?:\.(\d{1,6}))?$/.exec(value.trim());
|
|
586
|
+
if (!match) {
|
|
587
|
+
throw new ComputeCreditError(
|
|
588
|
+
"invalid_caps",
|
|
589
|
+
`Not a dollar amount: ${value} (use digits, at most 6 decimals)`
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
const atomic = BigInt(match[1]) * 10n ** BigInt(USDC_DECIMALS) + BigInt((match[2] ?? "").padEnd(USDC_DECIMALS, "0"));
|
|
593
|
+
if (atomic <= 0n) throw new ComputeCreditError("invalid_caps", "A cap must be above zero");
|
|
594
|
+
return atomic.toString();
|
|
595
|
+
}
|
|
596
|
+
function atomicToUsd(atomic) {
|
|
597
|
+
const value = BigInt(atomic);
|
|
598
|
+
const scale = 10n ** BigInt(USDC_DECIMALS);
|
|
599
|
+
const fraction = (value % scale).toString().padStart(USDC_DECIMALS, "0").replace(/0+$/, "");
|
|
600
|
+
return fraction ? `${value / scale}.${fraction}` : `${value / scale}`;
|
|
601
|
+
}
|
|
602
|
+
async function fetchComputePricing() {
|
|
603
|
+
const parsed = ComputeCreditPricingSchema.safeParse(await apiGet(COMPUTE_PRICING_PATH));
|
|
604
|
+
if (!parsed.success) {
|
|
605
|
+
throw new ComputeCreditError("invalid_response", "The compute pricing response is malformed");
|
|
606
|
+
}
|
|
607
|
+
return parsed.data;
|
|
608
|
+
}
|
|
609
|
+
function railOf(pricing) {
|
|
610
|
+
const rail = pricing.rails.find((entry) => entry.rail === "x402-exact-usdc");
|
|
611
|
+
if (!rail || !/^0x[0-9a-fA-F]{40}$/.test(rail.payTo ?? "")) {
|
|
612
|
+
throw new ComputeCreditError("no_rail", "The API offers no x402 USDC rail for compute credit");
|
|
613
|
+
}
|
|
614
|
+
return { network: rail.network, asset: rail.asset, payTo: rail.payTo };
|
|
615
|
+
}
|
|
616
|
+
function usdAtomic(amountUsd) {
|
|
617
|
+
return (BigInt(amountUsd) * 10n ** BigInt(USDC_DECIMALS)).toString();
|
|
618
|
+
}
|
|
619
|
+
function assertDenomination(pricing, amountUsd) {
|
|
620
|
+
if (!pricing.denominations.some((entry) => entry.amountUsd === amountUsd)) {
|
|
621
|
+
throw new ComputeCreditError(
|
|
622
|
+
"invalid_amount",
|
|
623
|
+
`$${amountUsd} is not a compute credit size; choose one of ${pricing.denominations.map((entry) => `$${entry.amountUsd}`).join(", ")}`
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
async function planComputeCreditSetup(input, pricing) {
|
|
628
|
+
const { network, asset } = railOf(pricing);
|
|
629
|
+
const perDay = BigInt(usdToAtomic(input.perDayUsd));
|
|
630
|
+
const perMonth = BigInt(usdToAtomic(input.perMonthUsd));
|
|
631
|
+
const total = BigInt(usdToAtomic(input.totalUsd ?? atomicToUsd((perMonth * 12n).toString())));
|
|
632
|
+
if (perDay > perMonth || perMonth > total) {
|
|
633
|
+
throw new ComputeCreditError(
|
|
634
|
+
"invalid_caps",
|
|
635
|
+
"The caps must satisfy per day <= per month <= total"
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
let autoTopUp;
|
|
639
|
+
if (input.autoTopUp) {
|
|
640
|
+
const { thresholdCredits, amountUsd, maxChunksPerDay } = input.autoTopUp;
|
|
641
|
+
assertDenomination(pricing, amountUsd);
|
|
642
|
+
if (!/^\d+(\.\d+)?$/.test(thresholdCredits) || Number(thresholdCredits) <= 0) {
|
|
643
|
+
throw new ComputeCreditError(
|
|
644
|
+
"invalid_caps",
|
|
645
|
+
"The top-up threshold must be credits above zero"
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
if (!Number.isInteger(maxChunksPerDay) || maxChunksPerDay < 1 || maxChunksPerDay > COMPUTE_AUTO_TOP_UP_MAX_CHUNKS_PER_DAY) {
|
|
649
|
+
throw new ComputeCreditError(
|
|
650
|
+
"invalid_caps",
|
|
651
|
+
`The daily chunk cap must be a whole number from 1 to ${COMPUTE_AUTO_TOP_UP_MAX_CHUNKS_PER_DAY}`
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
const autoPerDay = BigInt(amountUsd) * BigInt(maxChunksPerDay) * 10n ** BigInt(USDC_DECIMALS);
|
|
655
|
+
if (autoPerDay > perDay) {
|
|
656
|
+
throw new ComputeCreditError(
|
|
657
|
+
"invalid_caps",
|
|
658
|
+
"Auto-top-up chunks a day times the chunk size must fit within the daily cap"
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
autoTopUp = { ...input.autoTopUp, perDayUsd: atomicToUsd(autoPerDay.toString()) };
|
|
662
|
+
}
|
|
663
|
+
const existing = await loadComputeCreditConfig();
|
|
664
|
+
return {
|
|
665
|
+
network,
|
|
666
|
+
asset,
|
|
667
|
+
caps: {
|
|
668
|
+
perDayUsd: atomicToUsd(perDay.toString()),
|
|
669
|
+
perMonthUsd: atomicToUsd(perMonth.toString()),
|
|
670
|
+
totalUsd: atomicToUsd(total.toString())
|
|
671
|
+
},
|
|
672
|
+
...autoTopUp ? { autoTopUp } : {},
|
|
673
|
+
...existing ? { replaces: existing.allocationId } : {}
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
function allocations() {
|
|
677
|
+
return createX402Allocations({ journalPath: getX402JournalPath() });
|
|
678
|
+
}
|
|
679
|
+
async function applyComputeCreditSetup(plan, approvalNote, now = /* @__PURE__ */ new Date()) {
|
|
680
|
+
if (!approvalNote.trim()) {
|
|
681
|
+
throw new ComputeCreditError("not_approved", "Opening a compute allocation needs approval");
|
|
682
|
+
}
|
|
683
|
+
return withComputeCreditLock(() => applySetupLocked(plan, approvalNote, now));
|
|
684
|
+
}
|
|
685
|
+
async function applySetupLocked(plan, approvalNote, now) {
|
|
686
|
+
const replaces = (await loadComputeCreditConfig())?.allocationId;
|
|
687
|
+
const store = allocations();
|
|
688
|
+
const allocationId = `compute-credits-${randomUUID4()}`;
|
|
689
|
+
await store.open(
|
|
690
|
+
{
|
|
691
|
+
id: allocationId,
|
|
692
|
+
kind: "compute",
|
|
693
|
+
exposure: "compute",
|
|
694
|
+
network: plan.network,
|
|
695
|
+
asset: plan.asset,
|
|
696
|
+
caps: {
|
|
697
|
+
amount: usdToAtomic(plan.caps.totalUsd),
|
|
698
|
+
perDay: usdToAtomic(plan.caps.perDayUsd),
|
|
699
|
+
perMonth: usdToAtomic(plan.caps.perMonthUsd)
|
|
700
|
+
}
|
|
701
|
+
},
|
|
702
|
+
{ note: approvalNote }
|
|
703
|
+
);
|
|
704
|
+
let autoTopUp;
|
|
705
|
+
if (plan.autoTopUp) {
|
|
706
|
+
const childId = `${allocationId}-auto`;
|
|
707
|
+
await store.open({
|
|
708
|
+
id: childId,
|
|
709
|
+
kind: "compute",
|
|
710
|
+
exposure: "compute",
|
|
711
|
+
parentId: allocationId,
|
|
712
|
+
network: plan.network,
|
|
713
|
+
asset: plan.asset,
|
|
714
|
+
caps: {
|
|
715
|
+
amount: usdToAtomic(plan.caps.totalUsd),
|
|
716
|
+
perDay: usdToAtomic(plan.autoTopUp.perDayUsd)
|
|
717
|
+
}
|
|
718
|
+
});
|
|
719
|
+
autoTopUp = {
|
|
720
|
+
allocationId: childId,
|
|
721
|
+
thresholdCredits: plan.autoTopUp.thresholdCredits,
|
|
722
|
+
amountUsd: plan.autoTopUp.amountUsd,
|
|
723
|
+
maxChunksPerDay: plan.autoTopUp.maxChunksPerDay
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
const config = {
|
|
727
|
+
version: CONFIG_VERSION,
|
|
728
|
+
allocationId,
|
|
729
|
+
network: plan.network,
|
|
730
|
+
asset: plan.asset,
|
|
731
|
+
approvedAt: now.toISOString(),
|
|
732
|
+
...autoTopUp ? { autoTopUp } : {}
|
|
733
|
+
};
|
|
734
|
+
await writePrivateJson(getComputeCreditConfigPath(), config);
|
|
735
|
+
if (replaces && replaces !== allocationId) {
|
|
736
|
+
await store.close(replaces).catch((error) => {
|
|
737
|
+
if (error instanceof X402PayError && error.code === "allocation_not_found") return;
|
|
738
|
+
throw error;
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
return config;
|
|
742
|
+
}
|
|
743
|
+
async function disableComputeAutoTopUp() {
|
|
744
|
+
return withComputeCreditLock(async () => {
|
|
745
|
+
const config = await requireConfig();
|
|
746
|
+
if (config.autoTopUp) await allocations().close(config.autoTopUp.allocationId);
|
|
747
|
+
const rest = { ...config };
|
|
748
|
+
delete rest.autoTopUp;
|
|
749
|
+
await writePrivateJson(getComputeCreditConfigPath(), rest);
|
|
750
|
+
return rest;
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
async function requireConfig() {
|
|
754
|
+
const config = await loadComputeCreditConfig();
|
|
755
|
+
if (!config) {
|
|
756
|
+
throw new ComputeCreditError(
|
|
757
|
+
"not_set_up",
|
|
758
|
+
"No compute allocation yet. Run `taskmarket agent credits setup` to approve daily and monthly caps"
|
|
759
|
+
);
|
|
760
|
+
}
|
|
761
|
+
return config;
|
|
762
|
+
}
|
|
763
|
+
async function invocationStatus(invocationId, seams) {
|
|
764
|
+
const payer = createCliX402Payer({ firstParty: true, ...seams });
|
|
765
|
+
try {
|
|
766
|
+
return await payer.status(invocationId);
|
|
767
|
+
} catch (error) {
|
|
768
|
+
if (error instanceof X402PayError && error.code === "invocation_not_found") return void 0;
|
|
769
|
+
throw error;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
function provenUncharged(status) {
|
|
773
|
+
if (!status) return true;
|
|
774
|
+
if (status.state === "in_progress") return false;
|
|
775
|
+
return status.charge === "none" || status.charge === "released";
|
|
776
|
+
}
|
|
777
|
+
async function buyComputeCredit(input, seams = {}) {
|
|
778
|
+
return withComputeCreditLock(async () => {
|
|
779
|
+
const resolved = await resolvePendingLocked(seams);
|
|
780
|
+
if (!input.auto) {
|
|
781
|
+
const granted = resolved.find((outcome) => outcome.outcome === "granted");
|
|
782
|
+
if (granted?.outcome === "granted") {
|
|
783
|
+
throw new ComputeCreditError(
|
|
784
|
+
"earlier_purchase_granted",
|
|
785
|
+
`An earlier $${granted.purchase.amountUsd} purchase whose response was lost has been granted (${granted.purchase.credits} credits). Nothing new was bought; run buy again to buy more`
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
const pending = await listPendingComputePurchases();
|
|
789
|
+
if (pending.length > 0) {
|
|
790
|
+
const reasons = new Map(
|
|
791
|
+
resolved.flatMap(
|
|
792
|
+
(outcome) => outcome.outcome === "pending" ? [[outcome.idempotencyKey, outcome.reason]] : []
|
|
793
|
+
)
|
|
794
|
+
);
|
|
795
|
+
const described = pending.map(
|
|
796
|
+
(record) => `$${record.amountUsd} purchase ${record.idempotencyKey} from ${record.createdAt} (${reasons.get(record.idempotencyKey) ?? "unresolved"})`
|
|
797
|
+
).join("; ");
|
|
798
|
+
throw new ComputeCreditError(
|
|
799
|
+
"purchase_pending",
|
|
800
|
+
`Nothing new was bought: an earlier purchase may have been paid and is not settled yet: ${described}. Run \`taskmarket agent credits reconcile\` to settle it by its idempotency key, which never pays again. If it stays unresolved, \`taskmarket agent credits reconcile --release <key>\` stops waiting for it on this device; it is never resent, and the backend still grants it if it was paid`
|
|
801
|
+
);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
return { ...await purchaseLocked(input, seams), resolved };
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
async function purchaseLocked(input, seams) {
|
|
808
|
+
const config = await requireConfig();
|
|
809
|
+
const allocationId = input.auto ? config.autoTopUp?.allocationId : config.allocationId;
|
|
810
|
+
if (!allocationId) {
|
|
811
|
+
throw new ComputeCreditError("not_set_up", "Auto-top-up was not approved at setup");
|
|
812
|
+
}
|
|
813
|
+
const pricing = await fetchComputePricing();
|
|
814
|
+
assertDenomination(pricing, input.amountUsd);
|
|
815
|
+
const { payTo } = railOf(pricing);
|
|
816
|
+
const keystore = await loadKeystore();
|
|
817
|
+
const pending = {
|
|
818
|
+
idempotencyKey: randomUUID4(),
|
|
819
|
+
invocationId: randomUUID4(),
|
|
820
|
+
amountUsd: input.amountUsd,
|
|
821
|
+
auto: Boolean(input.auto),
|
|
822
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
823
|
+
};
|
|
824
|
+
await writePrivateJson(pendingPath(pending.idempotencyKey), pending);
|
|
825
|
+
try {
|
|
826
|
+
const outcome = await x402FeePost(
|
|
827
|
+
COMPUTE_CREDIT_PATH,
|
|
828
|
+
{ deviceId: keystore.deviceId, apiToken: keystore.apiToken, amountUsd: input.amountUsd },
|
|
829
|
+
{
|
|
830
|
+
idempotencyKey: pending.idempotencyKey,
|
|
831
|
+
invocationId: pending.invocationId,
|
|
832
|
+
allocationId,
|
|
833
|
+
expect: { amount: usdAtomic(input.amountUsd), payTo },
|
|
834
|
+
...seams
|
|
835
|
+
}
|
|
836
|
+
);
|
|
837
|
+
const parsed = ComputeCreditBuyOutputSchema.safeParse(outcome.data);
|
|
838
|
+
if (!parsed.success) {
|
|
839
|
+
throw new ComputeCreditError("invalid_response", "The purchase response is malformed");
|
|
840
|
+
}
|
|
841
|
+
await forgetPending(pending.idempotencyKey);
|
|
842
|
+
return parsed.data;
|
|
843
|
+
} catch (error) {
|
|
844
|
+
let uncharged = false;
|
|
845
|
+
try {
|
|
846
|
+
uncharged = provenUncharged(await invocationStatus(pending.invocationId, seams));
|
|
847
|
+
} catch {
|
|
848
|
+
}
|
|
849
|
+
if (uncharged) await forgetPending(pending.idempotencyKey);
|
|
850
|
+
throw error;
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
function apiErrorCode(error) {
|
|
854
|
+
return /\b(purchase_void|purchase_pending|in_flight|key_conflict|disabled)\b/.exec(
|
|
855
|
+
error.message
|
|
856
|
+
)?.[1];
|
|
857
|
+
}
|
|
858
|
+
async function resolvePendingComputePurchases(seams = {}) {
|
|
859
|
+
return withComputeCreditLock(() => resolvePendingLocked(seams));
|
|
860
|
+
}
|
|
861
|
+
async function resolvePendingLocked(seams) {
|
|
862
|
+
const records = await listPendingComputePurchases();
|
|
863
|
+
if (records.length === 0) return [];
|
|
864
|
+
const keystore = await loadKeystore();
|
|
865
|
+
const payer = createCliX402Payer({ firstParty: true, ...seams });
|
|
866
|
+
const outcomes = [];
|
|
867
|
+
for (const record of records) {
|
|
868
|
+
const key = record.idempotencyKey;
|
|
869
|
+
let status = await invocationStatus(record.invocationId, seams);
|
|
870
|
+
if (status?.state === "in_progress") {
|
|
871
|
+
outcomes.push({ idempotencyKey: key, outcome: "pending", reason: "in_progress" });
|
|
872
|
+
continue;
|
|
873
|
+
}
|
|
874
|
+
if (status?.uncertain || status?.charge === "reserved") {
|
|
875
|
+
status = await payer.reconcile(record.invocationId).catch(() => status);
|
|
876
|
+
}
|
|
877
|
+
if (provenUncharged(status)) {
|
|
878
|
+
await forgetPending(key);
|
|
879
|
+
outcomes.push({ idempotencyKey: key, outcome: "not_charged" });
|
|
880
|
+
continue;
|
|
881
|
+
}
|
|
882
|
+
try {
|
|
883
|
+
const replay = await apiPost(
|
|
884
|
+
COMPUTE_CREDIT_PATH,
|
|
885
|
+
{ deviceId: keystore.deviceId, apiToken: keystore.apiToken, amountUsd: record.amountUsd },
|
|
886
|
+
{ idempotencyKey: key }
|
|
887
|
+
);
|
|
888
|
+
const parsed = ComputeCreditBuyOutputSchema.safeParse(replay.data);
|
|
889
|
+
if (!parsed.success) {
|
|
890
|
+
outcomes.push({ idempotencyKey: key, outcome: "pending", reason: "invalid_response" });
|
|
891
|
+
continue;
|
|
892
|
+
}
|
|
893
|
+
await forgetPending(key);
|
|
894
|
+
outcomes.push({ idempotencyKey: key, outcome: "granted", purchase: parsed.data.purchase });
|
|
895
|
+
} catch (error) {
|
|
896
|
+
if (!(error instanceof ApiError)) throw error;
|
|
897
|
+
const code = apiErrorCode(error);
|
|
898
|
+
if (code === "purchase_void") {
|
|
899
|
+
await forgetPending(key);
|
|
900
|
+
outcomes.push({ idempotencyKey: key, outcome: "void" });
|
|
901
|
+
} else if (error.status === 402 && Date.now() - Date.parse(record.createdAt) > COMPUTE_PURCHASE_ARRIVAL_WINDOW_MS) {
|
|
902
|
+
await forgetPending(key);
|
|
903
|
+
outcomes.push({ idempotencyKey: key, outcome: "not_charged" });
|
|
904
|
+
} else {
|
|
905
|
+
outcomes.push({
|
|
906
|
+
idempotencyKey: key,
|
|
907
|
+
outcome: "pending",
|
|
908
|
+
reason: code ?? (error.status === 402 ? "awaiting_backend" : `http_${error.status}`)
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
return outcomes;
|
|
914
|
+
}
|
|
915
|
+
async function releasePendingComputePurchase(idempotencyKey, seams = {}) {
|
|
916
|
+
return withComputeCreditLock(async () => {
|
|
917
|
+
const record = (await listPendingComputePurchases()).find(
|
|
918
|
+
(entry) => entry.idempotencyKey === idempotencyKey
|
|
919
|
+
);
|
|
920
|
+
if (!record) {
|
|
921
|
+
throw new ComputeCreditError("not_pending", `No pending purchase ${idempotencyKey}`);
|
|
922
|
+
}
|
|
923
|
+
const status = await invocationStatus(record.invocationId, seams);
|
|
924
|
+
if (status?.state === "in_progress") {
|
|
925
|
+
throw new ComputeCreditError("busy", "That purchase is still being sent; try again shortly");
|
|
926
|
+
}
|
|
927
|
+
await forgetPending(idempotencyKey);
|
|
928
|
+
return record;
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
async function readComputeCreditBalance() {
|
|
932
|
+
return (await readStatus()).balance.availableCredits;
|
|
933
|
+
}
|
|
934
|
+
async function readStatus() {
|
|
935
|
+
const keystore = await loadKeystore();
|
|
936
|
+
const outcome = await apiPost(COMPUTE_CREDIT_STATUS_PATH, {
|
|
937
|
+
deviceId: keystore.deviceId,
|
|
938
|
+
apiToken: keystore.apiToken
|
|
939
|
+
});
|
|
940
|
+
const parsed = ComputeCreditStatusOutputSchema.safeParse(outcome.data);
|
|
941
|
+
if (!parsed.success) {
|
|
942
|
+
throw new ComputeCreditError("invalid_response", "The compute credit status is malformed");
|
|
943
|
+
}
|
|
944
|
+
return parsed.data;
|
|
945
|
+
}
|
|
946
|
+
async function readComputeCreditStatus() {
|
|
947
|
+
return readStatus();
|
|
948
|
+
}
|
|
949
|
+
async function allocationSummary(id) {
|
|
950
|
+
if (!id) return void 0;
|
|
951
|
+
const status = await allocations().get(id);
|
|
952
|
+
return {
|
|
953
|
+
allocationId: id,
|
|
954
|
+
closed: status.allocation.closedAt !== void 0,
|
|
955
|
+
caps: {
|
|
956
|
+
perDayUsd: status.allocation.caps.perDay && atomicToUsd(status.allocation.caps.perDay),
|
|
957
|
+
perMonthUsd: status.allocation.caps.perMonth && atomicToUsd(status.allocation.caps.perMonth),
|
|
958
|
+
totalUsd: atomicToUsd(status.allocation.caps.amount)
|
|
959
|
+
},
|
|
960
|
+
usedUsd: {
|
|
961
|
+
today: atomicToUsd(status.usage.day),
|
|
962
|
+
thisMonth: atomicToUsd(status.usage.month),
|
|
963
|
+
total: atomicToUsd(status.usage.used)
|
|
964
|
+
}
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
async function computeCreditStatus(seams = {}) {
|
|
968
|
+
const { resolved } = await withComputeCreditLock(async () => ({
|
|
969
|
+
resolved: await resolvePendingLocked(seams)
|
|
970
|
+
}));
|
|
971
|
+
const [status, pricing, config] = await Promise.all([
|
|
972
|
+
readStatus(),
|
|
973
|
+
fetchComputePricing(),
|
|
974
|
+
loadComputeCreditConfig()
|
|
975
|
+
]);
|
|
976
|
+
const topUp = await onComputeBalanceObserved(
|
|
977
|
+
{ availableCredits: status.balance.availableCredits },
|
|
978
|
+
seams
|
|
979
|
+
).catch(async (error) => ({ topUp: await failedTopUp(error, "check_failed") }));
|
|
980
|
+
const pending = await listPendingComputePurchases();
|
|
981
|
+
const after = topUp.topUp.action === "bought" ? await readStatus() : status;
|
|
982
|
+
return {
|
|
983
|
+
balance: after.balance,
|
|
984
|
+
topUp: topUp.topUp,
|
|
985
|
+
rate: {
|
|
986
|
+
rateCardVersion: pricing.rateCardVersion,
|
|
987
|
+
usdPerCredit: pricing.usdPerCredit,
|
|
988
|
+
salesApproval: pricing.salesApproval,
|
|
989
|
+
machineTypes: pricing.machineTypes
|
|
990
|
+
},
|
|
991
|
+
debits: after.debits,
|
|
992
|
+
purchases: after.purchases,
|
|
993
|
+
allocation: await allocationSummary(config?.allocationId),
|
|
994
|
+
autoTopUp: config?.autoTopUp ? {
|
|
995
|
+
thresholdCredits: config.autoTopUp.thresholdCredits,
|
|
996
|
+
amountUsd: config.autoTopUp.amountUsd,
|
|
997
|
+
maxChunksPerDay: config.autoTopUp.maxChunksPerDay,
|
|
998
|
+
allocation: await allocationSummary(config.autoTopUp.allocationId)
|
|
999
|
+
} : null,
|
|
1000
|
+
resolved,
|
|
1001
|
+
pending
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
function failureCode(error) {
|
|
1005
|
+
if (error instanceof X402PayError || error instanceof ComputeCreditError) return error.code;
|
|
1006
|
+
if (error instanceof ApiError) return apiErrorCode(error) ?? `http_${error.status}`;
|
|
1007
|
+
if (error instanceof TypeError) return "network";
|
|
1008
|
+
return "unknown";
|
|
1009
|
+
}
|
|
1010
|
+
async function failedTopUp(error, reason) {
|
|
1011
|
+
const pending = reason === "payment_failed" && (await listPendingComputePurchases()).length > 0;
|
|
1012
|
+
return {
|
|
1013
|
+
action: "skipped",
|
|
1014
|
+
reason: pending ? "outcome_unknown" : reason,
|
|
1015
|
+
code: failureCode(error),
|
|
1016
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
function decimalLess(a, b) {
|
|
1020
|
+
const scale = (value) => {
|
|
1021
|
+
const [whole, fraction = ""] = value.split(".");
|
|
1022
|
+
return BigInt(whole + fraction.padEnd(18, "0").slice(0, 18));
|
|
1023
|
+
};
|
|
1024
|
+
return scale(a) < scale(b);
|
|
1025
|
+
}
|
|
1026
|
+
async function checkComputeBalance(seams = {}) {
|
|
1027
|
+
return withComputeCreditLock(() => checkBalanceLocked(seams));
|
|
1028
|
+
}
|
|
1029
|
+
async function checkBalanceLocked(seams) {
|
|
1030
|
+
const resolved = await resolvePendingLocked(seams);
|
|
1031
|
+
const [status, config] = await Promise.all([readStatus(), loadComputeCreditConfig()]);
|
|
1032
|
+
let topUp;
|
|
1033
|
+
let balance = status.balance;
|
|
1034
|
+
const auto = config?.autoTopUp;
|
|
1035
|
+
if (!auto) {
|
|
1036
|
+
topUp = { action: "none", reason: "disabled" };
|
|
1037
|
+
} else if (!decimalLess(balance.availableCredits, auto.thresholdCredits)) {
|
|
1038
|
+
topUp = { action: "none", reason: "above_threshold" };
|
|
1039
|
+
} else if ((await listPendingComputePurchases()).length > 0) {
|
|
1040
|
+
topUp = { action: "none", reason: "purchase_pending" };
|
|
1041
|
+
} else {
|
|
1042
|
+
try {
|
|
1043
|
+
const bought = await purchaseLocked({ amountUsd: auto.amountUsd, auto: true }, seams);
|
|
1044
|
+
balance = bought.balance;
|
|
1045
|
+
topUp = { action: "bought", purchase: bought.purchase };
|
|
1046
|
+
} catch (error) {
|
|
1047
|
+
if (error instanceof X402PayError && error.code === "allocation_exhausted") {
|
|
1048
|
+
topUp = {
|
|
1049
|
+
action: "skipped",
|
|
1050
|
+
reason: error.allocation?.cap === "perDay" ? "daily_cap" : "allocation_cap"
|
|
1051
|
+
};
|
|
1052
|
+
} else if (error instanceof X402PayError && error.code === "allocation_closed") {
|
|
1053
|
+
topUp = { action: "skipped", reason: "allocation_closed" };
|
|
1054
|
+
} else {
|
|
1055
|
+
topUp = await failedTopUp(error, "payment_failed");
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
return { balance, topUp, resolved };
|
|
1060
|
+
}
|
|
1061
|
+
async function onComputeBalanceObserved(observation, seams = {}) {
|
|
1062
|
+
const auto = (await loadComputeCreditConfig())?.autoTopUp;
|
|
1063
|
+
if (!auto) return { topUp: { action: "none", reason: "disabled" } };
|
|
1064
|
+
const short = observation.creditWarning === "low_credit" || observation.creditWarning === "credit_exhausted";
|
|
1065
|
+
if (!short && (observation.availableCredits === void 0 || !decimalLess(observation.availableCredits, auto.thresholdCredits))) {
|
|
1066
|
+
return { topUp: { action: "none", reason: "above_threshold" } };
|
|
1067
|
+
}
|
|
1068
|
+
const checked = await checkComputeBalance(seams);
|
|
1069
|
+
return { topUp: checked.topUp, balance: checked.balance };
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
export {
|
|
1073
|
+
paidWriteRequest,
|
|
1074
|
+
refusedPaidWrite,
|
|
1075
|
+
failedAfterPayment,
|
|
1076
|
+
mppPost,
|
|
1077
|
+
loadExternalHeaders,
|
|
1078
|
+
executeX402PayRequest,
|
|
1079
|
+
reconcileX402PaymentById,
|
|
1080
|
+
x402FeePost,
|
|
1081
|
+
ComputeCreditError,
|
|
1082
|
+
loadComputeCreditConfig,
|
|
1083
|
+
listPendingComputePurchases,
|
|
1084
|
+
usdToAtomic,
|
|
1085
|
+
atomicToUsd,
|
|
1086
|
+
fetchComputePricing,
|
|
1087
|
+
assertDenomination,
|
|
1088
|
+
planComputeCreditSetup,
|
|
1089
|
+
applyComputeCreditSetup,
|
|
1090
|
+
disableComputeAutoTopUp,
|
|
1091
|
+
buyComputeCredit,
|
|
1092
|
+
resolvePendingComputePurchases,
|
|
1093
|
+
releasePendingComputePurchase,
|
|
1094
|
+
readComputeCreditBalance,
|
|
1095
|
+
readComputeCreditStatus,
|
|
1096
|
+
computeCreditStatus,
|
|
1097
|
+
checkComputeBalance,
|
|
1098
|
+
onComputeBalanceObserved
|
|
1099
|
+
};
|