@zeroxyz/cli 0.0.23 → 0.0.25
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/index.js +1260 -666
- package/package.json +1 -1
- package/skills/zero/SKILL.md +10 -0
package/dist/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/app.ts
|
|
4
|
-
import { Command as
|
|
4
|
+
import { Command as Command11 } from "commander";
|
|
5
5
|
|
|
6
6
|
// package.json
|
|
7
7
|
var package_default = {
|
|
8
8
|
name: "@zeroxyz/cli",
|
|
9
|
-
version: "0.0.
|
|
9
|
+
version: "0.0.25",
|
|
10
10
|
type: "module",
|
|
11
11
|
bin: {
|
|
12
12
|
zero: "dist/index.js",
|
|
@@ -59,21 +59,481 @@ var package_default = {
|
|
|
59
59
|
}
|
|
60
60
|
};
|
|
61
61
|
|
|
62
|
+
// src/commands/bug-report-command.ts
|
|
63
|
+
import { createHash as createHash2 } from "crypto";
|
|
64
|
+
import { readFileSync } from "fs";
|
|
65
|
+
import { Command } from "commander";
|
|
66
|
+
import { z as z2 } from "zod";
|
|
67
|
+
|
|
68
|
+
// src/services/api-service.ts
|
|
69
|
+
import { createHash } from "crypto";
|
|
70
|
+
import z from "zod";
|
|
71
|
+
var searchResultSchema = z.object({
|
|
72
|
+
id: z.string(),
|
|
73
|
+
position: z.number(),
|
|
74
|
+
slug: z.string(),
|
|
75
|
+
name: z.string(),
|
|
76
|
+
canonicalName: z.string().nullable().optional(),
|
|
77
|
+
description: z.string(),
|
|
78
|
+
whatItDoes: z.string().nullable().optional(),
|
|
79
|
+
url: z.string(),
|
|
80
|
+
cost: z.object({ amount: z.string(), asset: z.string() }),
|
|
81
|
+
rating: z.object({
|
|
82
|
+
score: z.string(),
|
|
83
|
+
successRate: z.string(),
|
|
84
|
+
reviews: z.number(),
|
|
85
|
+
stars: z.string().nullable().optional(),
|
|
86
|
+
state: z.enum(["unrated", "rated"]).optional()
|
|
87
|
+
}),
|
|
88
|
+
trustScore: z.number().nullable().optional(),
|
|
89
|
+
trustSignalCount: z.number().optional(),
|
|
90
|
+
availabilityStatus: z.enum(["healthy", "degraded", "down", "unknown"]).nullable().optional(),
|
|
91
|
+
relevanceScore: z.number().optional()
|
|
92
|
+
});
|
|
93
|
+
var searchResponseSchema = z.object({
|
|
94
|
+
searchId: z.string(),
|
|
95
|
+
total: z.number().optional().default(0),
|
|
96
|
+
offset: z.number().optional().default(0),
|
|
97
|
+
hasMore: z.boolean().optional().default(false),
|
|
98
|
+
capabilities: z.array(searchResultSchema)
|
|
99
|
+
});
|
|
100
|
+
var capabilityResponseSchema = z.object({
|
|
101
|
+
uid: z.string(),
|
|
102
|
+
slug: z.string(),
|
|
103
|
+
name: z.string(),
|
|
104
|
+
description: z.string(),
|
|
105
|
+
url: z.string(),
|
|
106
|
+
method: z.string(),
|
|
107
|
+
headers: z.record(z.string(), z.string()).nullable(),
|
|
108
|
+
bodySchema: z.record(z.string(), z.unknown()).nullable(),
|
|
109
|
+
responseSchema: z.record(z.string(), z.unknown()).nullable(),
|
|
110
|
+
example: z.object({ request: z.unknown(), response: z.unknown() }).nullable(),
|
|
111
|
+
tags: z.array(z.string()).nullable(),
|
|
112
|
+
displayCostAmount: z.string(),
|
|
113
|
+
displayCostAsset: z.string(),
|
|
114
|
+
rating: z.object({
|
|
115
|
+
score: z.string(),
|
|
116
|
+
successRate: z.string(),
|
|
117
|
+
reviews: z.number(),
|
|
118
|
+
stars: z.string().nullable().optional(),
|
|
119
|
+
state: z.enum(["unrated", "rated"]).optional()
|
|
120
|
+
}),
|
|
121
|
+
priceObserved: z.object({
|
|
122
|
+
medianCents: z.string(),
|
|
123
|
+
p95Cents: z.string(),
|
|
124
|
+
sampleCount: z.number(),
|
|
125
|
+
varies: z.boolean()
|
|
126
|
+
}).nullable().optional(),
|
|
127
|
+
paymentMethods: z.array(
|
|
128
|
+
z.object({
|
|
129
|
+
uid: z.string(),
|
|
130
|
+
protocol: z.string(),
|
|
131
|
+
methodType: z.string(),
|
|
132
|
+
chain: z.string().nullable(),
|
|
133
|
+
mode: z.string(),
|
|
134
|
+
costAmount: z.string(),
|
|
135
|
+
costPer: z.string(),
|
|
136
|
+
priority: z.number()
|
|
137
|
+
})
|
|
138
|
+
).nullable(),
|
|
139
|
+
trustScore: z.number().nullable().optional(),
|
|
140
|
+
trustComponents: z.object({
|
|
141
|
+
apiQuality: z.number().nullable(),
|
|
142
|
+
blockchainActivity: z.number().nullable(),
|
|
143
|
+
performance: z.number().nullable()
|
|
144
|
+
}).nullable().optional(),
|
|
145
|
+
availabilityStatus: z.enum(["healthy", "degraded", "down", "unknown"]).nullable().optional()
|
|
146
|
+
});
|
|
147
|
+
var createRunResponseSchema = z.object({
|
|
148
|
+
runId: z.string()
|
|
149
|
+
});
|
|
150
|
+
var createReviewResponseSchema = z.object({
|
|
151
|
+
reviewId: z.string(),
|
|
152
|
+
recorded: z.boolean()
|
|
153
|
+
});
|
|
154
|
+
var BUG_REPORT_CATEGORIES = [
|
|
155
|
+
"search_relevance",
|
|
156
|
+
"ranking_issue",
|
|
157
|
+
"missing_capability",
|
|
158
|
+
"wrong_schema",
|
|
159
|
+
"misleading_description",
|
|
160
|
+
"broken_execution",
|
|
161
|
+
"payment_failure",
|
|
162
|
+
"billing_anomaly",
|
|
163
|
+
"cli_bug",
|
|
164
|
+
"security",
|
|
165
|
+
"other"
|
|
166
|
+
];
|
|
167
|
+
var createBugReportResponseSchema = z.object({
|
|
168
|
+
bugReportId: z.string(),
|
|
169
|
+
status: z.string(),
|
|
170
|
+
deduped: z.boolean(),
|
|
171
|
+
category: z.enum(BUG_REPORT_CATEGORIES).nullable(),
|
|
172
|
+
title: z.string().nullable(),
|
|
173
|
+
attached: z.object({
|
|
174
|
+
capabilityId: z.number().nullable(),
|
|
175
|
+
runId: z.number().nullable(),
|
|
176
|
+
searchId: z.number().nullable()
|
|
177
|
+
})
|
|
178
|
+
});
|
|
179
|
+
var runListItemSchema = z.object({
|
|
180
|
+
uid: z.string(),
|
|
181
|
+
capabilityUid: z.string(),
|
|
182
|
+
capabilitySlug: z.string(),
|
|
183
|
+
capabilityName: z.string(),
|
|
184
|
+
status: z.number().nullable(),
|
|
185
|
+
latencyMs: z.number().nullable(),
|
|
186
|
+
cost: z.object({ amount: z.string(), asset: z.string().nullable() }).nullable(),
|
|
187
|
+
payment: z.object({
|
|
188
|
+
protocol: z.string(),
|
|
189
|
+
chain: z.string().nullable(),
|
|
190
|
+
txHash: z.string().nullable(),
|
|
191
|
+
mode: z.string().nullable()
|
|
192
|
+
}).nullable(),
|
|
193
|
+
createdAt: z.coerce.date(),
|
|
194
|
+
reviewed: z.boolean()
|
|
195
|
+
});
|
|
196
|
+
var listRunsResponseSchema = z.object({
|
|
197
|
+
runs: z.array(runListItemSchema),
|
|
198
|
+
nextCursor: z.string().nullable()
|
|
199
|
+
});
|
|
200
|
+
var buildCanonicalMessage = (method, path, body, timestamp, nonce) => {
|
|
201
|
+
const bodyHash = createHash("sha256").update(body ?? "").digest("hex");
|
|
202
|
+
return `${method}:${path}:${bodyHash}:${timestamp}:${nonce}`;
|
|
203
|
+
};
|
|
204
|
+
var ApiService = class {
|
|
205
|
+
constructor(baseUrl, account) {
|
|
206
|
+
this.baseUrl = baseUrl;
|
|
207
|
+
this.account = account;
|
|
208
|
+
this.walletAddress = this.account?.address ?? null;
|
|
209
|
+
}
|
|
210
|
+
walletAddress;
|
|
211
|
+
account;
|
|
212
|
+
signRequest = async (method, path, body) => {
|
|
213
|
+
if (!this.account) throw new Error("No private key configured");
|
|
214
|
+
const timestamp = Math.floor(Date.now() / 1e3).toString();
|
|
215
|
+
const nonce = crypto.randomUUID();
|
|
216
|
+
const message = buildCanonicalMessage(method, path, body, timestamp, nonce);
|
|
217
|
+
const signature = await this.account.signMessage({ message });
|
|
218
|
+
return {
|
|
219
|
+
"x-zero-address": this.account.address,
|
|
220
|
+
"x-zero-timestamp": timestamp,
|
|
221
|
+
"x-zero-nonce": nonce,
|
|
222
|
+
"x-zero-signature": signature
|
|
223
|
+
};
|
|
224
|
+
};
|
|
225
|
+
request = async (method, path, body) => {
|
|
226
|
+
const url = `${this.baseUrl}${path}`;
|
|
227
|
+
const bodyStr = body ? JSON.stringify(body) : void 0;
|
|
228
|
+
const headers = {
|
|
229
|
+
"content-type": "application/json"
|
|
230
|
+
};
|
|
231
|
+
if (this.account) {
|
|
232
|
+
const walletHeaders = await this.signRequest(method, path, bodyStr);
|
|
233
|
+
Object.assign(headers, walletHeaders);
|
|
234
|
+
}
|
|
235
|
+
const response = await fetch(url, {
|
|
236
|
+
method,
|
|
237
|
+
headers,
|
|
238
|
+
body: bodyStr
|
|
239
|
+
});
|
|
240
|
+
if (!response.ok) {
|
|
241
|
+
const errorBody = await response.json().catch(() => ({ error: "Unknown error" }));
|
|
242
|
+
throw new Error(
|
|
243
|
+
errorBody.error ?? `HTTP ${response.status}`
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
return response.json();
|
|
247
|
+
};
|
|
248
|
+
search = async (options) => {
|
|
249
|
+
const json = await this.request("POST", "/v1/search", options);
|
|
250
|
+
return searchResponseSchema.parse(json);
|
|
251
|
+
};
|
|
252
|
+
getCapability = async (id, searchId) => {
|
|
253
|
+
const qs = searchId ? `?searchId=${encodeURIComponent(searchId)}` : "";
|
|
254
|
+
const json = await this.request(
|
|
255
|
+
"GET",
|
|
256
|
+
`/v1/capabilities/${encodeURIComponent(id)}${qs}`
|
|
257
|
+
);
|
|
258
|
+
return capabilityResponseSchema.parse(json);
|
|
259
|
+
};
|
|
260
|
+
createRun = async (data) => {
|
|
261
|
+
const json = await this.request("POST", "/v1/runs", data);
|
|
262
|
+
return createRunResponseSchema.parse(json);
|
|
263
|
+
};
|
|
264
|
+
listRuns = async (params = {}) => {
|
|
265
|
+
const qs = new URLSearchParams();
|
|
266
|
+
if (params.capabilityId) qs.set("capabilityId", params.capabilityId);
|
|
267
|
+
if (params.unreviewed) qs.set("unreviewed", "true");
|
|
268
|
+
if (params.limit) qs.set("limit", String(params.limit));
|
|
269
|
+
if (params.cursor) qs.set("cursor", params.cursor);
|
|
270
|
+
const suffix = qs.toString() ? `?${qs.toString()}` : "";
|
|
271
|
+
const json = await this.request("GET", `/v1/runs${suffix}`);
|
|
272
|
+
return listRunsResponseSchema.parse(json);
|
|
273
|
+
};
|
|
274
|
+
createReview = async (data) => {
|
|
275
|
+
const json = await this.request("POST", "/v1/reviews", data);
|
|
276
|
+
return createReviewResponseSchema.parse(json);
|
|
277
|
+
};
|
|
278
|
+
createBugReport = async (data) => {
|
|
279
|
+
const json = await this.request("POST", "/v1/bug-reports", data);
|
|
280
|
+
return createBugReportResponseSchema.parse(json);
|
|
281
|
+
};
|
|
282
|
+
getFundingUrl = async (amount, provider = "coinbase") => {
|
|
283
|
+
try {
|
|
284
|
+
const params = new URLSearchParams({ provider });
|
|
285
|
+
if (amount) params.set("amount", amount);
|
|
286
|
+
const json = await this.request(
|
|
287
|
+
"GET",
|
|
288
|
+
`/v1/wallet/fund-url?${params.toString()}`
|
|
289
|
+
);
|
|
290
|
+
const parsed = z.object({ url: z.string() }).parse(json);
|
|
291
|
+
return parsed.url;
|
|
292
|
+
} catch {
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
// src/commands/bug-report-command.ts
|
|
299
|
+
var bulkEntrySchema = z2.object({
|
|
300
|
+
description: z2.string().min(1),
|
|
301
|
+
category: z2.enum(BUG_REPORT_CATEGORIES).optional(),
|
|
302
|
+
title: z2.string().min(1).max(200).optional(),
|
|
303
|
+
severity: z2.number().int().min(1).max(3).optional(),
|
|
304
|
+
reproduction: z2.string().optional(),
|
|
305
|
+
capabilityId: z2.string().optional(),
|
|
306
|
+
runId: z2.string().optional(),
|
|
307
|
+
searchId: z2.string().optional(),
|
|
308
|
+
idempotencyKey: z2.string().min(1).max(200).optional()
|
|
309
|
+
});
|
|
310
|
+
var buildCliContext = () => ({
|
|
311
|
+
cliVersion: package_default.version,
|
|
312
|
+
os: process.platform,
|
|
313
|
+
node: process.version
|
|
314
|
+
});
|
|
315
|
+
var TEN_MIN_MS = 10 * 60 * 1e3;
|
|
316
|
+
var autoIdempotencyKey = (description, contextSeed) => {
|
|
317
|
+
const bucket = Math.floor(Date.now() / TEN_MIN_MS);
|
|
318
|
+
const seed = `${description}|${contextSeed ?? ""}|${bucket}`;
|
|
319
|
+
return `auto-${createHash2("sha256").update(seed).digest("hex").slice(0, 16)}`;
|
|
320
|
+
};
|
|
321
|
+
var bugReportCommand = (appContext) => new Command("bug-report").description(
|
|
322
|
+
"Report a Zero platform bug \u2014 bad search ranking, wrong indexed URL, CLI bugs, billing issues. The CLI auto-attaches your most recent context (last search + last run), auto-derives a title, and lets the server classify the category. For 'this capability returned a bad result', use `zero review` instead."
|
|
323
|
+
).addHelpText(
|
|
324
|
+
"after",
|
|
325
|
+
`
|
|
326
|
+
Minimum surface \u2014 just describe the bug:
|
|
327
|
+
zero bug-report "Search ranked generic crypto API above exact match for BTC price"
|
|
328
|
+
|
|
329
|
+
The CLI will:
|
|
330
|
+
- Attach your most recent search + run as context (use --no-context to skip)
|
|
331
|
+
- Auto-derive a title from your description
|
|
332
|
+
- Generate an idempotency key so accidental retries dedupe within 10 minutes
|
|
333
|
+
- Capture cliContext (cli version, os, node)
|
|
334
|
+
|
|
335
|
+
The server will:
|
|
336
|
+
- Pick a category from your description (override with --category if it gets it wrong)
|
|
337
|
+
- Default severity to 2 (override with --severity)
|
|
338
|
+
- Echo back what it picked + attached so you can verify
|
|
339
|
+
|
|
340
|
+
Overrides (only when needed):
|
|
341
|
+
--capability cap_xyz Attach a specific capability (overrides auto-context)
|
|
342
|
+
--run run_xyz Attach a specific run
|
|
343
|
+
--search sea_xyz Attach a specific search
|
|
344
|
+
--no-context Skip auto-attached context (file a standalone platform bug)
|
|
345
|
+
--category <c> Force a category (otherwise classifier picks)
|
|
346
|
+
--severity 1|2|3 Override default severity 2
|
|
347
|
+
--title "..." Override auto-derived title
|
|
348
|
+
--reproduction "..." Add reproduction notes
|
|
349
|
+
--idempotency-key <k> Override auto-key (e.g. for explicit retries)
|
|
350
|
+
--json Machine-readable output
|
|
351
|
+
--from-file <path> Bulk submit from JSONL (each line: {description, ...overrides})
|
|
352
|
+
|
|
353
|
+
Categories the classifier picks from:
|
|
354
|
+
search & discovery: search_relevance, ranking_issue, missing_capability
|
|
355
|
+
indexed-data quality: wrong_schema, misleading_description, broken_execution
|
|
356
|
+
platform infra: payment_failure, billing_anomaly, cli_bug
|
|
357
|
+
cross-cutting: security, other`
|
|
358
|
+
).argument(
|
|
359
|
+
"[description]",
|
|
360
|
+
"What broke (free-form text). Required unless --from-file is used."
|
|
361
|
+
).option(
|
|
362
|
+
"--capability <id>",
|
|
363
|
+
"Attach this capability uid (cap_*) or slug as context, overriding auto-context"
|
|
364
|
+
).option(
|
|
365
|
+
"--run <runId>",
|
|
366
|
+
"Attach this run uid (run_*) as context, overriding auto-context"
|
|
367
|
+
).option(
|
|
368
|
+
"--search <searchId>",
|
|
369
|
+
"Attach this search uid (sea_*) as context, overriding auto-context"
|
|
370
|
+
).option("--no-context", "Skip auto-attached context entirely").option(
|
|
371
|
+
"--category <category>",
|
|
372
|
+
`Force a category (skips classifier). One of: ${BUG_REPORT_CATEGORIES.join(", ")}`
|
|
373
|
+
).option(
|
|
374
|
+
"--severity <n>",
|
|
375
|
+
"Severity 1 (low), 2 (med, default), 3 (high)",
|
|
376
|
+
Number.parseInt
|
|
377
|
+
).option("--title <text>", "Override auto-derived title (\u2264200 chars)").option(
|
|
378
|
+
"--reproduction <text>",
|
|
379
|
+
"Optional reproduction steps (e.g. equivalent curl)"
|
|
380
|
+
).option("--idempotency-key <key>", "Override the auto-generated dedup key").option(
|
|
381
|
+
"--from-file <path>",
|
|
382
|
+
"Submit bug reports in bulk from a JSONL file (one report per line)"
|
|
383
|
+
).option("--json", "Emit the API result as JSON on stdout (for batch use)").action(
|
|
384
|
+
async (description, options) => {
|
|
385
|
+
try {
|
|
386
|
+
const { analyticsService, apiService, stateService } = appContext.services;
|
|
387
|
+
if (!apiService.walletAddress) {
|
|
388
|
+
console.error(
|
|
389
|
+
"Wallet auth required \u2014 run `zero wallet import` or set ZERO_PRIVATE_KEY."
|
|
390
|
+
);
|
|
391
|
+
process.exitCode = 1;
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
if (options.fromFile) {
|
|
395
|
+
const contents = readFileSync(options.fromFile, "utf8");
|
|
396
|
+
const lines = contents.split("\n").map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
|
|
397
|
+
let ok = 0;
|
|
398
|
+
let failed = 0;
|
|
399
|
+
const cliContext = buildCliContext();
|
|
400
|
+
for (const [idx, line] of lines.entries()) {
|
|
401
|
+
const lineNum = idx + 1;
|
|
402
|
+
try {
|
|
403
|
+
const parsed = bulkEntrySchema.parse(JSON.parse(line));
|
|
404
|
+
const result2 = await apiService.createBugReport({
|
|
405
|
+
description: parsed.description,
|
|
406
|
+
category: parsed.category,
|
|
407
|
+
severity: parsed.severity,
|
|
408
|
+
title: parsed.title,
|
|
409
|
+
reproduction: parsed.reproduction,
|
|
410
|
+
capabilityId: parsed.capabilityId,
|
|
411
|
+
runId: parsed.runId,
|
|
412
|
+
searchId: parsed.searchId,
|
|
413
|
+
cliContext,
|
|
414
|
+
idempotencyKey: parsed.idempotencyKey ?? autoIdempotencyKey(parsed.description, parsed.runId)
|
|
415
|
+
});
|
|
416
|
+
ok += 1;
|
|
417
|
+
console.log(
|
|
418
|
+
`[${lineNum}] ${result2.bugReportId} category=${result2.category ?? "unclassified"}${result2.deduped ? " (deduped)" : ""}`
|
|
419
|
+
);
|
|
420
|
+
analyticsService.capture("bug_report_submitted", {
|
|
421
|
+
category: result2.category,
|
|
422
|
+
severity: parsed.severity ?? 2,
|
|
423
|
+
bulk: true,
|
|
424
|
+
deduped: result2.deduped
|
|
425
|
+
});
|
|
426
|
+
} catch (err) {
|
|
427
|
+
failed += 1;
|
|
428
|
+
console.error(
|
|
429
|
+
`[${lineNum}] FAILED: ${err instanceof Error ? err.message : String(err)}`
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
console.log(
|
|
434
|
+
`
|
|
435
|
+
Bulk bug-report complete: ${ok} ok, ${failed} failed`
|
|
436
|
+
);
|
|
437
|
+
if (failed > 0) process.exitCode = 1;
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
if (!description || description.trim().length === 0) {
|
|
441
|
+
console.error(
|
|
442
|
+
'Provide a description: zero bug-report "what broke"'
|
|
443
|
+
);
|
|
444
|
+
process.exitCode = 1;
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
if (options.category && !BUG_REPORT_CATEGORIES.includes(
|
|
448
|
+
options.category
|
|
449
|
+
)) {
|
|
450
|
+
console.error(
|
|
451
|
+
`Invalid --category. Valid: ${BUG_REPORT_CATEGORIES.join(", ")}`
|
|
452
|
+
);
|
|
453
|
+
process.exitCode = 1;
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
if (options.severity !== void 0 && (Number.isNaN(options.severity) || options.severity < 1 || options.severity > 3)) {
|
|
457
|
+
console.error("--severity must be 1, 2, or 3");
|
|
458
|
+
process.exitCode = 1;
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
let { capability, run, search } = options;
|
|
462
|
+
const useAutoContext = options.context !== false;
|
|
463
|
+
if (useAutoContext) {
|
|
464
|
+
const lastSearch = stateService.loadLastSearch();
|
|
465
|
+
if (lastSearch) {
|
|
466
|
+
search = search ?? lastSearch.searchId;
|
|
467
|
+
const recent = lastSearch.capabilities[0];
|
|
468
|
+
capability = capability ?? recent?.id;
|
|
469
|
+
}
|
|
470
|
+
if (!run) {
|
|
471
|
+
try {
|
|
472
|
+
const list = await apiService.listRuns({ limit: 1 });
|
|
473
|
+
const [latest] = list.runs;
|
|
474
|
+
if (latest) {
|
|
475
|
+
run = latest.uid;
|
|
476
|
+
capability = capability ?? latest.capabilityUid;
|
|
477
|
+
}
|
|
478
|
+
} catch {
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
const idempotencyKey = options.idempotencyKey ?? autoIdempotencyKey(description, run ?? capability ?? null);
|
|
483
|
+
const result = await apiService.createBugReport({
|
|
484
|
+
description,
|
|
485
|
+
category: options.category,
|
|
486
|
+
severity: options.severity,
|
|
487
|
+
title: options.title,
|
|
488
|
+
reproduction: options.reproduction,
|
|
489
|
+
capabilityId: capability,
|
|
490
|
+
runId: run,
|
|
491
|
+
searchId: search,
|
|
492
|
+
cliContext: buildCliContext(),
|
|
493
|
+
idempotencyKey
|
|
494
|
+
});
|
|
495
|
+
if (options.json) {
|
|
496
|
+
console.log(JSON.stringify(result));
|
|
497
|
+
} else {
|
|
498
|
+
const cat = result.category ?? "unclassified";
|
|
499
|
+
const dedupeNote = result.deduped ? " (deduped \u2014 same description within 10 min)" : "";
|
|
500
|
+
console.log(
|
|
501
|
+
`Bug report filed: ${result.bugReportId}${dedupeNote}
|
|
502
|
+
category: ${cat}
|
|
503
|
+
title: ${result.title ?? "(none)"}
|
|
504
|
+
attached: capability=${result.attached.capabilityId ?? "\u2014"} run=${result.attached.runId ?? "\u2014"} search=${result.attached.searchId ?? "\u2014"}`
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
analyticsService.capture("bug_report_submitted", {
|
|
508
|
+
category: result.category,
|
|
509
|
+
severity: options.severity ?? 2,
|
|
510
|
+
deduped: result.deduped,
|
|
511
|
+
autoContext: useAutoContext
|
|
512
|
+
});
|
|
513
|
+
} catch (err) {
|
|
514
|
+
console.error(
|
|
515
|
+
err instanceof Error ? err.message : "Bug report failed"
|
|
516
|
+
);
|
|
517
|
+
process.exitCode = 1;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
);
|
|
521
|
+
|
|
62
522
|
// src/commands/config-command.ts
|
|
63
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
523
|
+
import { existsSync, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
64
524
|
import { homedir } from "os";
|
|
65
525
|
import { join } from "path";
|
|
66
|
-
import { Command } from "commander";
|
|
526
|
+
import { Command as Command2 } from "commander";
|
|
67
527
|
var VALID_KEYS = ["lowBalanceWarning", "auth", "telemetry"];
|
|
68
528
|
var loadConfig = (configPath) => {
|
|
69
529
|
try {
|
|
70
530
|
if (!existsSync(configPath)) return {};
|
|
71
|
-
return JSON.parse(
|
|
531
|
+
return JSON.parse(readFileSync2(configPath, "utf8"));
|
|
72
532
|
} catch {
|
|
73
533
|
return {};
|
|
74
534
|
}
|
|
75
535
|
};
|
|
76
|
-
var configCommand = (_appContext) => new
|
|
536
|
+
var configCommand = (_appContext) => new Command2("config").description("View or update CLI configuration").option("--set <keyValue>", "Set a config value (key=value)").action((options) => {
|
|
77
537
|
const configPath = join(homedir(), ".zero", "config.json");
|
|
78
538
|
if (!options.set) {
|
|
79
539
|
const config2 = loadConfig(configPath);
|
|
@@ -110,128 +570,637 @@ var configCommand = (_appContext) => new Command("config").description("View or
|
|
|
110
570
|
});
|
|
111
571
|
|
|
112
572
|
// src/commands/fetch-command.ts
|
|
113
|
-
import { Command as
|
|
573
|
+
import { Command as Command3 } from "commander";
|
|
574
|
+
import { formatUnits as formatUnits2 } from "viem";
|
|
114
575
|
|
|
115
|
-
// src/
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
576
|
+
// src/services/payment-service.ts
|
|
577
|
+
import {
|
|
578
|
+
adaptViemWallet,
|
|
579
|
+
convertViemChainToRelayChain,
|
|
580
|
+
createClient as createRelayClient,
|
|
581
|
+
getClient as getRelayClient,
|
|
582
|
+
MAINNET_RELAY_API
|
|
583
|
+
} from "@relayprotocol/relay-sdk";
|
|
584
|
+
import { x402Client as X402Client } from "@x402/core/client";
|
|
585
|
+
import { decodePaymentResponseHeader, x402HTTPClient } from "@x402/core/http";
|
|
586
|
+
import { ExactEvmScheme } from "@x402/evm/exact/client";
|
|
587
|
+
import { createSIWxClientHook } from "@x402/extensions/sign-in-with-x";
|
|
588
|
+
import { wrapFetchWithPayment } from "@x402/fetch";
|
|
589
|
+
import { Challenge, Receipt } from "mppx";
|
|
590
|
+
import { Mppx, tempo } from "mppx/client";
|
|
591
|
+
import {
|
|
592
|
+
createPublicClient,
|
|
593
|
+
createWalletClient,
|
|
594
|
+
formatUnits,
|
|
595
|
+
http
|
|
596
|
+
} from "viem";
|
|
597
|
+
import { base, baseSepolia } from "viem/chains";
|
|
598
|
+
var SessionCloseFailedError = class extends Error {
|
|
599
|
+
session;
|
|
600
|
+
response;
|
|
601
|
+
capturedAmount;
|
|
602
|
+
constructor(params) {
|
|
603
|
+
super(params.message);
|
|
604
|
+
this.name = "SessionCloseFailedError";
|
|
605
|
+
this.session = params.session;
|
|
606
|
+
this.response = params.response;
|
|
607
|
+
this.capturedAmount = params.capturedAmount;
|
|
135
608
|
}
|
|
136
|
-
return { type: typeOf(value) };
|
|
137
609
|
};
|
|
138
|
-
var
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
610
|
+
var USDC_BASE = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913";
|
|
611
|
+
var USDC_BASE_SEPOLIA = "0x036CbD53842c5426634e7929541eC2318f3dCF7e";
|
|
612
|
+
var USDC_TEMPO = "0x20c000000000000000000000b9537d11c60e8b50";
|
|
613
|
+
var PATHUSD_TEMPO = "0x20c0000000000000000000000000000000000000";
|
|
614
|
+
var BASE_CHAIN_ID = 8453;
|
|
615
|
+
var TEMPO_CHAIN_ID = 4217;
|
|
616
|
+
var TEMPO_TESTNET_CHAIN_ID = 42431;
|
|
617
|
+
var DEFAULT_MAX_DEPOSIT = "100";
|
|
618
|
+
var KNOWN_EIP712_DOMAINS = {
|
|
619
|
+
// USDC on Base
|
|
620
|
+
[USDC_BASE.toLowerCase()]: { name: "USDC", version: "2" },
|
|
621
|
+
// USDC on Base Sepolia
|
|
622
|
+
[USDC_BASE_SEPOLIA.toLowerCase()]: { name: "USDC", version: "2" }
|
|
142
623
|
};
|
|
143
|
-
var
|
|
624
|
+
var buildRelayClientOptions = () => ({
|
|
625
|
+
baseApiUrl: MAINNET_RELAY_API,
|
|
626
|
+
source: "zero-cli",
|
|
627
|
+
chains: [convertViemChainToRelayChain(base)]
|
|
628
|
+
});
|
|
629
|
+
var calculateBuffer = (baseBalance) => {
|
|
630
|
+
const twentyFivePercent = baseBalance / 4n;
|
|
631
|
+
const twoDollars = 2000000n;
|
|
632
|
+
return twentyFivePercent < twoDollars ? twentyFivePercent : twoDollars;
|
|
633
|
+
};
|
|
634
|
+
var tempoChain = {
|
|
635
|
+
id: TEMPO_CHAIN_ID,
|
|
636
|
+
name: "Tempo",
|
|
637
|
+
nativeCurrency: { name: "USD", symbol: "USD", decimals: 18 },
|
|
638
|
+
rpcUrls: {
|
|
639
|
+
default: { http: ["https://rpc.tempo.xyz"] }
|
|
640
|
+
}
|
|
641
|
+
};
|
|
642
|
+
var tempoTestnetChain = {
|
|
643
|
+
id: TEMPO_TESTNET_CHAIN_ID,
|
|
644
|
+
name: "Tempo Testnet",
|
|
645
|
+
nativeCurrency: { name: "USD", symbol: "USD", decimals: 18 },
|
|
646
|
+
rpcUrls: {
|
|
647
|
+
default: { http: ["https://rpc.moderato.tempo.xyz"] }
|
|
648
|
+
}
|
|
649
|
+
};
|
|
650
|
+
var ERC20_BALANCE_ABI = [
|
|
651
|
+
{
|
|
652
|
+
inputs: [{ name: "account", type: "address" }],
|
|
653
|
+
name: "balanceOf",
|
|
654
|
+
outputs: [{ name: "", type: "uint256" }],
|
|
655
|
+
stateMutability: "view",
|
|
656
|
+
type: "function"
|
|
657
|
+
}
|
|
658
|
+
];
|
|
659
|
+
var decodeSessionReceiptHeader = (header) => {
|
|
660
|
+
if (!header) return null;
|
|
144
661
|
try {
|
|
145
|
-
|
|
662
|
+
const padLen = (4 - header.length % 4) % 4;
|
|
663
|
+
const padded = header.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat(padLen);
|
|
664
|
+
const json = Buffer.from(padded, "base64").toString("utf8");
|
|
665
|
+
const parsed = JSON.parse(json);
|
|
666
|
+
if (typeof parsed.channelId !== "string" || typeof parsed.acceptedCumulative !== "string" || typeof parsed.spent !== "string" || typeof parsed.challengeId !== "string") {
|
|
667
|
+
return null;
|
|
668
|
+
}
|
|
669
|
+
return parsed;
|
|
146
670
|
} catch {
|
|
147
671
|
return null;
|
|
148
672
|
}
|
|
149
673
|
};
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
674
|
+
var readSessionReceipt = (response) => decodeSessionReceiptHeader(
|
|
675
|
+
response.headers.get("Payment-Receipt") ?? response.headers.get("payment-receipt")
|
|
676
|
+
);
|
|
677
|
+
var pickSessionCloseAmount = (receipt, openTimeCumulative) => {
|
|
678
|
+
if (!receipt) return openTimeCumulative;
|
|
679
|
+
const accepted = BigInt(receipt.acceptedCumulative);
|
|
680
|
+
const spent = BigInt(receipt.spent);
|
|
681
|
+
const fromReceipt = accepted > spent ? accepted : spent;
|
|
682
|
+
return fromReceipt > openTimeCumulative ? fromReceipt : openTimeCumulative;
|
|
683
|
+
};
|
|
684
|
+
var PaymentService = class {
|
|
685
|
+
constructor(account, config, deps = {}) {
|
|
686
|
+
this.account = account;
|
|
687
|
+
this.config = config;
|
|
688
|
+
this.fetchOverride = deps.fetchImpl;
|
|
689
|
+
}
|
|
690
|
+
relayInitialized = false;
|
|
691
|
+
fetchOverride;
|
|
692
|
+
/**
|
|
693
|
+
* Resolve the fetch implementation lazily so tests can `vi.stubGlobal`
|
|
694
|
+
* the global `fetch` after the service is constructed.
|
|
695
|
+
*/
|
|
696
|
+
get fetchImpl() {
|
|
697
|
+
return this.fetchOverride ?? globalThis.fetch;
|
|
698
|
+
}
|
|
699
|
+
getAccount = () => this.account;
|
|
700
|
+
ensureRelayClient = () => {
|
|
701
|
+
if (!this.relayInitialized) {
|
|
702
|
+
createRelayClient(buildRelayClientOptions());
|
|
703
|
+
this.relayInitialized = true;
|
|
704
|
+
}
|
|
705
|
+
};
|
|
706
|
+
bridgeToTempo = async (requiredAmount, onProgress) => {
|
|
707
|
+
if (!this.account) throw new Error("No wallet configured");
|
|
708
|
+
this.ensureRelayClient();
|
|
709
|
+
const baseBalance = await this.getBalanceRaw("base");
|
|
710
|
+
const buffer = calculateBuffer(baseBalance);
|
|
711
|
+
const bridgeAmount = requiredAmount + buffer;
|
|
712
|
+
if (baseBalance < bridgeAmount) {
|
|
713
|
+
throw new Error(
|
|
714
|
+
`Insufficient Base USDC to bridge: have ${formatUnits(baseBalance, 6)}, need ${formatUnits(bridgeAmount, 6)} (${formatUnits(requiredAmount, 6)} + ${formatUnits(buffer, 6)} buffer)`
|
|
159
715
|
);
|
|
160
|
-
return { protocol: "x402", raw: decoded };
|
|
161
|
-
} catch {
|
|
162
|
-
return { protocol: "x402", raw: { encoded: x402Header } };
|
|
163
716
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
if (options.header) {
|
|
193
|
-
for (const h of options.header) {
|
|
194
|
-
const colonIdx = h.indexOf(":");
|
|
195
|
-
if (colonIdx > 0) {
|
|
196
|
-
headers[h.slice(0, colonIdx).trim()] = h.slice(colonIdx + 1).trim();
|
|
197
|
-
}
|
|
717
|
+
onProgress?.(
|
|
718
|
+
`Bridging ${formatUnits(bridgeAmount, 6)} USDC from Base to Tempo...`
|
|
719
|
+
);
|
|
720
|
+
const walletClient = createWalletClient({
|
|
721
|
+
account: this.account,
|
|
722
|
+
chain: base,
|
|
723
|
+
transport: http()
|
|
724
|
+
});
|
|
725
|
+
const quote = await getRelayClient().actions.getQuote({
|
|
726
|
+
chainId: BASE_CHAIN_ID,
|
|
727
|
+
toChainId: TEMPO_CHAIN_ID,
|
|
728
|
+
currency: USDC_BASE,
|
|
729
|
+
toCurrency: USDC_TEMPO,
|
|
730
|
+
amount: bridgeAmount.toString(),
|
|
731
|
+
tradeType: "EXACT_INPUT",
|
|
732
|
+
user: this.account.address,
|
|
733
|
+
recipient: this.account.address,
|
|
734
|
+
options: {
|
|
735
|
+
usePermit: true
|
|
736
|
+
}
|
|
737
|
+
});
|
|
738
|
+
let bridgeTxHash = null;
|
|
739
|
+
await getRelayClient().actions.execute({
|
|
740
|
+
quote,
|
|
741
|
+
wallet: adaptViemWallet(walletClient),
|
|
742
|
+
onProgress: ({ txHashes }) => {
|
|
743
|
+
if (txHashes?.length && !bridgeTxHash) {
|
|
744
|
+
bridgeTxHash = txHashes[0]?.txHash ?? null;
|
|
198
745
|
}
|
|
199
746
|
}
|
|
200
|
-
|
|
201
|
-
|
|
747
|
+
});
|
|
748
|
+
return bridgeTxHash;
|
|
749
|
+
};
|
|
750
|
+
handlePayment = async (url, request, paymentRequirement, maxPay, onProgress) => {
|
|
751
|
+
if (!this.account) {
|
|
752
|
+
throw new Error(
|
|
753
|
+
"No wallet configured \u2014 run `zero init` or set ZERO_PRIVATE_KEY"
|
|
202
754
|
);
|
|
203
|
-
|
|
204
|
-
|
|
755
|
+
}
|
|
756
|
+
if (paymentRequirement.protocol === "x402") {
|
|
757
|
+
onProgress?.("Paying via x402 on Base...");
|
|
758
|
+
return this.payX402(url, request, paymentRequirement.raw, maxPay);
|
|
759
|
+
}
|
|
760
|
+
if (paymentRequirement.protocol === "mpp") {
|
|
761
|
+
onProgress?.("Paying via MPP on Tempo...");
|
|
762
|
+
return this.payMpp(
|
|
763
|
+
url,
|
|
764
|
+
request,
|
|
765
|
+
paymentRequirement.raw,
|
|
766
|
+
maxPay,
|
|
767
|
+
onProgress
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
throw new Error("Unrecognized 402 payment protocol");
|
|
771
|
+
};
|
|
772
|
+
payX402 = async (url, request, _raw, maxPay) => {
|
|
773
|
+
if (!this.account) throw new Error("No wallet configured");
|
|
774
|
+
let capturedAmount = "0";
|
|
775
|
+
const client = new X402Client().register(
|
|
776
|
+
"eip155:*",
|
|
777
|
+
new ExactEvmScheme(this.account)
|
|
778
|
+
);
|
|
779
|
+
client.onBeforePaymentCreation(async (context) => {
|
|
780
|
+
const selected = context.selectedRequirements;
|
|
781
|
+
if (selected && (!selected.extra?.name || !selected.extra?.version)) {
|
|
782
|
+
const known = KNOWN_EIP712_DOMAINS[selected.asset?.toLowerCase() ?? ""];
|
|
783
|
+
if (known) {
|
|
784
|
+
selected.extra = { ...selected.extra, ...known };
|
|
785
|
+
}
|
|
205
786
|
}
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
787
|
+
const requirement = context.paymentRequired.accepts[0];
|
|
788
|
+
if (!requirement) return;
|
|
789
|
+
capturedAmount = formatUnits(BigInt(requirement.amount), 6);
|
|
790
|
+
if (maxPay && Number.parseFloat(capturedAmount) > Number.parseFloat(maxPay)) {
|
|
791
|
+
return {
|
|
792
|
+
abort: true,
|
|
793
|
+
reason: `Payment of ${capturedAmount} USDC exceeds --max-pay ${maxPay}`
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
});
|
|
797
|
+
const httpClient = new x402HTTPClient(client).onPaymentRequired(
|
|
798
|
+
createSIWxClientHook(this.account)
|
|
799
|
+
);
|
|
800
|
+
const wrappedFetch = wrapFetchWithPayment(fetch, httpClient);
|
|
801
|
+
const response = await wrappedFetch(url, {
|
|
802
|
+
method: request.method,
|
|
803
|
+
headers: request.headers,
|
|
804
|
+
body: request.body
|
|
805
|
+
});
|
|
806
|
+
let txHash = null;
|
|
807
|
+
const paymentResponseHeader = response.headers.get("payment-response") ?? response.headers.get("x-payment-response");
|
|
808
|
+
if (paymentResponseHeader) {
|
|
809
|
+
try {
|
|
810
|
+
const settlement = decodePaymentResponseHeader(paymentResponseHeader);
|
|
811
|
+
txHash = settlement.transaction ?? null;
|
|
812
|
+
} catch {
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
return {
|
|
816
|
+
response,
|
|
817
|
+
protocol: "x402",
|
|
818
|
+
chain: "base",
|
|
819
|
+
txHash,
|
|
820
|
+
amount: capturedAmount,
|
|
821
|
+
asset: "USDC"
|
|
822
|
+
};
|
|
823
|
+
};
|
|
824
|
+
/**
|
|
825
|
+
* Shared pre-payment work: compute the Tempo amount we need, enforce
|
|
826
|
+
* --max-pay, and bridge from Base USDC if the Tempo balance is short.
|
|
827
|
+
* Invoked from mppx's `onChallenge` — i.e. AFTER the server's 402 and
|
|
828
|
+
* BEFORE mppx signs a credential — so balance/bridge logic runs in-band
|
|
829
|
+
* without adding a pre-probe round-trip.
|
|
830
|
+
*/
|
|
831
|
+
prepareTempoFunds = async (challenge, maxPay, onProgress) => {
|
|
832
|
+
const challengeRequest = challenge.request;
|
|
833
|
+
let requiredRaw;
|
|
834
|
+
if (challenge.intent === "session") {
|
|
835
|
+
const suggestedDeposit = challengeRequest.suggestedDeposit;
|
|
836
|
+
requiredRaw = suggestedDeposit ? BigInt(suggestedDeposit) : BigInt(
|
|
837
|
+
Math.floor(
|
|
838
|
+
Number.parseFloat(maxPay ?? DEFAULT_MAX_DEPOSIT) * 1e6
|
|
839
|
+
)
|
|
216
840
|
);
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
841
|
+
} else {
|
|
842
|
+
requiredRaw = BigInt(challengeRequest.amount);
|
|
843
|
+
}
|
|
844
|
+
const capturedAmount = formatUnits(requiredRaw, 6);
|
|
845
|
+
if (maxPay && Number.parseFloat(capturedAmount) > Number.parseFloat(maxPay)) {
|
|
846
|
+
throw new Error(
|
|
847
|
+
`Payment of ${capturedAmount} USDC exceeds --max-pay ${maxPay}`
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
const methodDetails = challengeRequest.methodDetails;
|
|
851
|
+
const challengeChainId = challengeRequest.chainId ?? methodDetails?.chainId;
|
|
852
|
+
const isTestnet = challengeChainId === TEMPO_TESTNET_CHAIN_ID;
|
|
853
|
+
onProgress?.(`Checking Tempo balance...`);
|
|
854
|
+
const tempoBalance = await this.getBalanceRaw(
|
|
855
|
+
isTestnet ? "tempo-testnet" : "tempo"
|
|
856
|
+
);
|
|
857
|
+
if (tempoBalance < requiredRaw) {
|
|
858
|
+
if (isTestnet) {
|
|
859
|
+
throw new Error(
|
|
860
|
+
`Insufficient pathUSD on Tempo testnet: have ${formatUnits(tempoBalance, 6)}, need ${capturedAmount}. Fund your wallet with the Tempo testnet faucet: https://docs.tempo.xyz/quickstart/faucet`
|
|
223
861
|
);
|
|
224
862
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
863
|
+
await this.bridgeToTempo(requiredRaw, onProgress);
|
|
864
|
+
}
|
|
865
|
+
return capturedAmount;
|
|
866
|
+
};
|
|
867
|
+
/**
|
|
868
|
+
* MPP payment entrypoint. Runs a SINGLE `mppx.fetch` and uses mppx's
|
|
869
|
+
* `onChallenge` callback to run balance/bridge/max-pay logic in-band
|
|
870
|
+
* with the server's 402. After the 200 comes back, we branch on the
|
|
871
|
+
* captured challenge intent + whether the tempo method reported the
|
|
872
|
+
* channel as opened (only session lifecycle fires `onChannelUpdate`).
|
|
873
|
+
*
|
|
874
|
+
* Why no pre-probe: a pre-probe adds an extra unauthenticated POST
|
|
875
|
+
* before mppx's own 402 dance (2 → 3 server-side requests). Session-
|
|
876
|
+
* intent facilitators that re-run the LLM per request can't reconcile
|
|
877
|
+
* the extra state and return `410 "channel not found"` on the close.
|
|
878
|
+
* This mirrors the working production v0.0.21 single-fetch flow.
|
|
879
|
+
*/
|
|
880
|
+
payMpp = async (url, request, _raw, maxPay, onProgress) => {
|
|
881
|
+
const account = this.account;
|
|
882
|
+
if (!account) throw new Error("No wallet configured");
|
|
883
|
+
let capturedAmount = "0";
|
|
884
|
+
let capturedChallenge;
|
|
885
|
+
let channelEntry;
|
|
886
|
+
const mppx = Mppx.create({
|
|
887
|
+
polyfill: false,
|
|
888
|
+
fetch: this.fetchImpl,
|
|
889
|
+
methods: [
|
|
890
|
+
tempo({
|
|
891
|
+
account,
|
|
892
|
+
maxDeposit: maxPay ?? DEFAULT_MAX_DEPOSIT,
|
|
893
|
+
onChannelUpdate: (entry) => {
|
|
894
|
+
channelEntry = {
|
|
895
|
+
channelId: entry.channelId,
|
|
896
|
+
escrowContract: entry.escrowContract,
|
|
897
|
+
cumulativeAmount: entry.cumulativeAmount,
|
|
898
|
+
opened: entry.opened
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
})
|
|
902
|
+
],
|
|
903
|
+
onChallenge: async (challenge) => {
|
|
904
|
+
capturedChallenge = challenge;
|
|
905
|
+
capturedAmount = await this.prepareTempoFunds(
|
|
906
|
+
challenge,
|
|
907
|
+
maxPay,
|
|
908
|
+
onProgress
|
|
228
909
|
);
|
|
910
|
+
return void 0;
|
|
229
911
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
912
|
+
});
|
|
913
|
+
const response = await mppx.fetch(url, {
|
|
914
|
+
method: request.method,
|
|
915
|
+
headers: request.headers,
|
|
916
|
+
body: request.body
|
|
917
|
+
});
|
|
918
|
+
if (capturedChallenge?.intent === "session" && channelEntry?.opened) {
|
|
919
|
+
return this.completeMppSession({
|
|
920
|
+
url,
|
|
921
|
+
request,
|
|
922
|
+
challenge: capturedChallenge,
|
|
923
|
+
channelEntry,
|
|
924
|
+
response,
|
|
925
|
+
capturedAmount,
|
|
926
|
+
mppx
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
let txHash = null;
|
|
930
|
+
try {
|
|
931
|
+
const receipt = Receipt.fromResponse(response);
|
|
932
|
+
txHash = receipt.reference ?? null;
|
|
933
|
+
} catch {
|
|
934
|
+
}
|
|
935
|
+
return {
|
|
936
|
+
response,
|
|
937
|
+
protocol: "mpp",
|
|
938
|
+
chain: "tempo",
|
|
939
|
+
txHash,
|
|
940
|
+
amount: capturedAmount,
|
|
941
|
+
asset: "USDC"
|
|
942
|
+
};
|
|
943
|
+
};
|
|
944
|
+
/**
|
|
945
|
+
* Close an opened MPP session channel after the seller returns a 200.
|
|
946
|
+
*
|
|
947
|
+
* Decodes the server's `Payment-Receipt` to get the settled
|
|
948
|
+
* `acceptedCumulative`/`spent`, signs a close voucher at that amount
|
|
949
|
+
* (never below on-chain settled), and POSTs it back to the seller.
|
|
950
|
+
* Needed for dynamic-pricing sellers whose open-time `amount` is `0` —
|
|
951
|
+
* signing close with `cumulativeAmount=0` would be rejected with
|
|
952
|
+
* `voucher cumulativeAmount is below on-chain settled amount`.
|
|
953
|
+
*
|
|
954
|
+
* When the seller's facilitator rejects the close (e.g. strict matching
|
|
955
|
+
* of fresh challenge IDs per request), `SessionCloseFailedError` is
|
|
956
|
+
* thrown carrying the session metadata so the caller can record the run
|
|
957
|
+
* against the orphaned channel.
|
|
958
|
+
*/
|
|
959
|
+
completeMppSession = async (params) => {
|
|
960
|
+
const {
|
|
961
|
+
url,
|
|
962
|
+
request,
|
|
963
|
+
challenge,
|
|
964
|
+
channelEntry,
|
|
965
|
+
response,
|
|
966
|
+
capturedAmount,
|
|
967
|
+
mppx
|
|
968
|
+
} = params;
|
|
969
|
+
const challengeRequest = challenge.request;
|
|
970
|
+
const recipient = challengeRequest.recipient;
|
|
971
|
+
const methodDetails = challengeRequest.methodDetails;
|
|
972
|
+
const challengeEscrow = methodDetails?.escrowContract;
|
|
973
|
+
const challengeChainId = challengeRequest.chainId ?? methodDetails?.chainId;
|
|
974
|
+
if (!recipient || !challengeEscrow) {
|
|
975
|
+
throw new Error("session challenge missing recipient/escrowContract");
|
|
976
|
+
}
|
|
977
|
+
if (!challengeChainId) {
|
|
978
|
+
throw new Error("session challenge missing chainId");
|
|
979
|
+
}
|
|
980
|
+
const {
|
|
981
|
+
channelId,
|
|
982
|
+
escrowContract,
|
|
983
|
+
cumulativeAmount: openTimeCumulative
|
|
984
|
+
} = channelEntry;
|
|
985
|
+
const receipt = readSessionReceipt(response);
|
|
986
|
+
const closeAmount = pickSessionCloseAmount(receipt, openTimeCumulative);
|
|
987
|
+
const orphanedSession = {
|
|
988
|
+
channelId,
|
|
989
|
+
escrowContract,
|
|
990
|
+
chainId: challengeChainId,
|
|
991
|
+
recipient,
|
|
992
|
+
cumulativeAmount: closeAmount.toString()
|
|
993
|
+
};
|
|
994
|
+
const closeFailed = (message) => new SessionCloseFailedError({
|
|
995
|
+
message,
|
|
996
|
+
session: orphanedSession,
|
|
997
|
+
response,
|
|
998
|
+
capturedAmount
|
|
999
|
+
});
|
|
1000
|
+
const closeChallengeResponse = new Response("", {
|
|
1001
|
+
status: 402,
|
|
1002
|
+
headers: { "WWW-Authenticate": Challenge.serialize(challenge) }
|
|
1003
|
+
});
|
|
1004
|
+
let closeCredential;
|
|
1005
|
+
try {
|
|
1006
|
+
closeCredential = await mppx.createCredential(closeChallengeResponse, {
|
|
1007
|
+
action: "close",
|
|
1008
|
+
channelId,
|
|
1009
|
+
cumulativeAmountRaw: closeAmount.toString()
|
|
1010
|
+
});
|
|
1011
|
+
} catch (err) {
|
|
1012
|
+
throw closeFailed(
|
|
1013
|
+
`Session close credential build failed for channel ${channelId}: ${err instanceof Error ? err.message : String(err)}`
|
|
1014
|
+
);
|
|
1015
|
+
}
|
|
1016
|
+
const closeMethod = request.method.toUpperCase();
|
|
1017
|
+
const methodCarriesBody = closeMethod === "POST" || closeMethod === "PUT" || closeMethod === "PATCH";
|
|
1018
|
+
const closeResponse = await this.fetchImpl(url, {
|
|
1019
|
+
method: closeMethod,
|
|
1020
|
+
headers: {
|
|
1021
|
+
...request.headers,
|
|
1022
|
+
// biome-ignore lint/style/useNamingConvention: HTTP header name
|
|
1023
|
+
Authorization: closeCredential
|
|
1024
|
+
},
|
|
1025
|
+
...methodCarriesBody && request.body ? { body: request.body } : {}
|
|
1026
|
+
});
|
|
1027
|
+
if (!closeResponse.ok) {
|
|
1028
|
+
const body = await closeResponse.text().catch(() => "");
|
|
1029
|
+
throw closeFailed(
|
|
1030
|
+
`Seller rejected close credential (status ${closeResponse.status}${body ? `: ${body.slice(0, 200)}` : ""}) for channel ${channelId}`
|
|
1031
|
+
);
|
|
1032
|
+
}
|
|
1033
|
+
const closeReceipt = readSessionReceipt(closeResponse);
|
|
1034
|
+
return {
|
|
1035
|
+
response,
|
|
1036
|
+
protocol: "mpp",
|
|
1037
|
+
chain: "tempo",
|
|
1038
|
+
txHash: null,
|
|
1039
|
+
amount: capturedAmount,
|
|
1040
|
+
asset: "USDC",
|
|
1041
|
+
session: {
|
|
1042
|
+
channelId,
|
|
1043
|
+
escrowContract,
|
|
1044
|
+
chainId: challengeChainId,
|
|
1045
|
+
recipient,
|
|
1046
|
+
cumulativeAmount: closeReceipt?.acceptedCumulative ?? closeAmount.toString()
|
|
1047
|
+
}
|
|
1048
|
+
};
|
|
1049
|
+
};
|
|
1050
|
+
resolveChainConfig = (chain) => {
|
|
1051
|
+
switch (chain) {
|
|
1052
|
+
case "base":
|
|
1053
|
+
return { viemChain: base, token: USDC_BASE };
|
|
1054
|
+
case "base-sepolia":
|
|
1055
|
+
return { viemChain: baseSepolia, token: USDC_BASE_SEPOLIA };
|
|
1056
|
+
case "tempo":
|
|
1057
|
+
return { viemChain: tempoChain, token: USDC_TEMPO };
|
|
1058
|
+
case "tempo-testnet":
|
|
1059
|
+
return { viemChain: tempoTestnetChain, token: PATHUSD_TEMPO };
|
|
1060
|
+
}
|
|
1061
|
+
};
|
|
1062
|
+
getBalanceRaw = async (chain) => {
|
|
1063
|
+
if (!this.account) return 0n;
|
|
1064
|
+
const { viemChain, token } = this.resolveChainConfig(chain);
|
|
1065
|
+
const client = createPublicClient({
|
|
1066
|
+
chain: viemChain,
|
|
1067
|
+
transport: http()
|
|
1068
|
+
});
|
|
1069
|
+
const balance = await client.readContract({
|
|
1070
|
+
address: token,
|
|
1071
|
+
abi: ERC20_BALANCE_ABI,
|
|
1072
|
+
functionName: "balanceOf",
|
|
1073
|
+
args: [this.account.address]
|
|
1074
|
+
});
|
|
1075
|
+
return balance;
|
|
1076
|
+
};
|
|
1077
|
+
getBalance = async (chain) => {
|
|
1078
|
+
const raw = await this.getBalanceRaw(chain);
|
|
1079
|
+
return { amount: formatUnits(raw, 6), asset: "USDC" };
|
|
1080
|
+
};
|
|
1081
|
+
};
|
|
1082
|
+
|
|
1083
|
+
// src/util/infer-schema.ts
|
|
1084
|
+
var inferSchema = (value, depth = 0) => {
|
|
1085
|
+
if (depth > 6) return { type: typeOf(value) };
|
|
1086
|
+
if (value === null) return { type: "null" };
|
|
1087
|
+
if (Array.isArray(value)) {
|
|
1088
|
+
const itemSchemas = value.slice(0, 3).map((v) => inferSchema(v, depth + 1));
|
|
1089
|
+
return {
|
|
1090
|
+
type: "array",
|
|
1091
|
+
items: itemSchemas[0] ?? { type: "unknown" }
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
if (typeof value === "object") {
|
|
1095
|
+
const obj = value;
|
|
1096
|
+
const properties = {};
|
|
1097
|
+
const required = [];
|
|
1098
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
1099
|
+
properties[k] = inferSchema(v, depth + 1);
|
|
1100
|
+
required.push(k);
|
|
1101
|
+
}
|
|
1102
|
+
return { type: "object", properties, required };
|
|
1103
|
+
}
|
|
1104
|
+
return { type: typeOf(value) };
|
|
1105
|
+
};
|
|
1106
|
+
var typeOf = (v) => {
|
|
1107
|
+
if (v === null) return "null";
|
|
1108
|
+
if (Array.isArray(v)) return "array";
|
|
1109
|
+
return typeof v;
|
|
1110
|
+
};
|
|
1111
|
+
var tryParseJson = (text) => {
|
|
1112
|
+
try {
|
|
1113
|
+
return JSON.parse(text);
|
|
1114
|
+
} catch {
|
|
1115
|
+
return null;
|
|
1116
|
+
}
|
|
1117
|
+
};
|
|
1118
|
+
|
|
1119
|
+
// src/commands/fetch-command.ts
|
|
1120
|
+
var detectPaymentRequirement = (headers, status) => {
|
|
1121
|
+
if (status !== 402) return null;
|
|
1122
|
+
const x402Header = headers.get("payment-required") ?? headers.get("x-payment-required");
|
|
1123
|
+
if (x402Header) {
|
|
1124
|
+
try {
|
|
1125
|
+
const decoded = JSON.parse(
|
|
1126
|
+
Buffer.from(x402Header, "base64").toString("utf8")
|
|
1127
|
+
);
|
|
1128
|
+
return { protocol: "x402", raw: decoded };
|
|
1129
|
+
} catch {
|
|
1130
|
+
return { protocol: "x402", raw: { encoded: x402Header } };
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
const wwwAuth = headers.get("www-authenticate");
|
|
1134
|
+
if (wwwAuth?.toLowerCase().includes("payment")) {
|
|
1135
|
+
return { protocol: "mpp", raw: { "www-authenticate": wwwAuth } };
|
|
1136
|
+
}
|
|
1137
|
+
return { protocol: "unknown", raw: {} };
|
|
1138
|
+
};
|
|
1139
|
+
var fetchCommand = (appContext) => new Command3("fetch").description("Fetch a capability URL with automatic payment handling").argument("<url>", "URL to fetch").option(
|
|
1140
|
+
"-X, --method <method>",
|
|
1141
|
+
"HTTP method (GET, POST, PUT, PATCH, DELETE). Defaults to POST when -d is set, otherwise GET"
|
|
1142
|
+
).option("-d, --data <body>", "Request body (JSON string)").option("-H, --header <header...>", "Headers in Key:Value format").option("--max-pay <amount>", "Maximum amount willing to pay (USDC)").option(
|
|
1143
|
+
"--capability <id>",
|
|
1144
|
+
"Bind this fetch to a capability (uid or slug) so a reviewable run is recorded even without a prior `zero search`"
|
|
1145
|
+
).option(
|
|
1146
|
+
"--json",
|
|
1147
|
+
"Emit {runId, status, latencyMs, payment, body} as JSON on stdout (for batch/non-TTY use)"
|
|
1148
|
+
).action(
|
|
1149
|
+
async (url, options) => {
|
|
1150
|
+
try {
|
|
1151
|
+
const {
|
|
1152
|
+
analyticsService,
|
|
1153
|
+
apiService,
|
|
1154
|
+
paymentService,
|
|
1155
|
+
stateService,
|
|
1156
|
+
walletService
|
|
1157
|
+
} = appContext.services;
|
|
1158
|
+
const startTime = Date.now();
|
|
1159
|
+
const headers = {};
|
|
1160
|
+
if (options.header) {
|
|
1161
|
+
for (const h of options.header) {
|
|
1162
|
+
const colonIdx = h.indexOf(":");
|
|
1163
|
+
if (colonIdx > 0) {
|
|
1164
|
+
headers[h.slice(0, colonIdx).trim()] = h.slice(colonIdx + 1).trim();
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
const hasContentType = Object.keys(headers).some(
|
|
1169
|
+
(k) => k.toLowerCase() === "content-type"
|
|
1170
|
+
);
|
|
1171
|
+
if (options.data && !hasContentType) {
|
|
1172
|
+
headers["content-type"] = "application/json";
|
|
1173
|
+
}
|
|
1174
|
+
const log = (msg) => console.error(` ${msg}`);
|
|
1175
|
+
const method = options.method ? options.method.toUpperCase() : options.data ? "POST" : "GET";
|
|
1176
|
+
const requestInit = {
|
|
1177
|
+
method,
|
|
1178
|
+
headers,
|
|
1179
|
+
body: options.data
|
|
1180
|
+
};
|
|
1181
|
+
const lastSearch = stateService.loadLastSearch();
|
|
1182
|
+
const matchedCapability = lastSearch?.capabilities.find(
|
|
1183
|
+
(c) => url.startsWith(c.url)
|
|
1184
|
+
);
|
|
1185
|
+
const capabilityId = options.capability ?? matchedCapability?.id ?? null;
|
|
1186
|
+
const searchId = matchedCapability ? lastSearch?.searchId : void 0;
|
|
1187
|
+
const skipReasons = [];
|
|
1188
|
+
if (!apiService.walletAddress) {
|
|
1189
|
+
skipReasons.push(
|
|
1190
|
+
"no wallet configured (run `zero wallet import` / set ZERO_PRIVATE_KEY)"
|
|
1191
|
+
);
|
|
1192
|
+
}
|
|
1193
|
+
if (!capabilityId) {
|
|
1194
|
+
skipReasons.push(
|
|
1195
|
+
"no capability resolved \u2014 pass --capability <uid|slug> or run `zero search` first so the URL can be matched"
|
|
1196
|
+
);
|
|
1197
|
+
}
|
|
1198
|
+
let finalResponse;
|
|
1199
|
+
let body = "";
|
|
1200
|
+
let paymentMeta;
|
|
1201
|
+
let sessionMeta;
|
|
1202
|
+
let fetchError;
|
|
1203
|
+
try {
|
|
235
1204
|
log(`Calling ${url}...`);
|
|
236
1205
|
const response = await fetch(url, requestInit);
|
|
237
1206
|
const paymentReq = detectPaymentRequirement(
|
|
@@ -255,17 +1224,50 @@ var fetchCommand = (appContext) => new Command2("fetch").description("Fetch a ca
|
|
|
255
1224
|
chain: result.chain,
|
|
256
1225
|
txHash: result.txHash,
|
|
257
1226
|
amount: result.amount,
|
|
258
|
-
asset: result.asset
|
|
1227
|
+
asset: result.asset,
|
|
1228
|
+
...result.session && { session: result.session }
|
|
259
1229
|
};
|
|
260
1230
|
log(
|
|
261
1231
|
`Paid ${result.amount} ${result.asset} via ${result.protocol} on ${result.chain}`
|
|
262
1232
|
);
|
|
1233
|
+
if (result.session) {
|
|
1234
|
+
sessionMeta = result.session;
|
|
1235
|
+
log(
|
|
1236
|
+
`MPP session closed \u2014 channel ${result.session.channelId.slice(0, 10)}... \u2192 ${result.session.recipient.slice(0, 8)}... (${formatUnits2(BigInt(result.session.cumulativeAmount), 6)} USDC settled)`
|
|
1237
|
+
);
|
|
1238
|
+
}
|
|
263
1239
|
} else {
|
|
264
1240
|
finalResponse = response;
|
|
265
1241
|
}
|
|
266
1242
|
body = await finalResponse.text();
|
|
267
1243
|
} catch (err) {
|
|
268
|
-
|
|
1244
|
+
if (err instanceof SessionCloseFailedError) {
|
|
1245
|
+
finalResponse = err.response;
|
|
1246
|
+
body = await err.response.text().catch(() => "");
|
|
1247
|
+
paymentMeta = {
|
|
1248
|
+
protocol: "mpp",
|
|
1249
|
+
chain: "tempo",
|
|
1250
|
+
txHash: null,
|
|
1251
|
+
amount: err.capturedAmount,
|
|
1252
|
+
asset: "USDC",
|
|
1253
|
+
session: err.session
|
|
1254
|
+
};
|
|
1255
|
+
sessionMeta = err.session;
|
|
1256
|
+
fetchError = err;
|
|
1257
|
+
console.error(
|
|
1258
|
+
[
|
|
1259
|
+
"",
|
|
1260
|
+
" WARNING: failed to auto-close MPP session.",
|
|
1261
|
+
` ${err.message}`,
|
|
1262
|
+
` Channel ${err.session.channelId} is still open on chain \u2014 see payment.session in --json output.`,
|
|
1263
|
+
" Contact the seller to settle the channel from their facilitator, or use",
|
|
1264
|
+
" your wallet to call escrow.close/requestClose directly.",
|
|
1265
|
+
""
|
|
1266
|
+
].join("\n")
|
|
1267
|
+
);
|
|
1268
|
+
} else {
|
|
1269
|
+
fetchError = err instanceof Error ? err : new Error(String(err));
|
|
1270
|
+
}
|
|
269
1271
|
}
|
|
270
1272
|
const latencyMs = Date.now() - startTime;
|
|
271
1273
|
if (finalResponse && !options.json) {
|
|
@@ -326,7 +1328,7 @@ var fetchCommand = (appContext) => new Command2("fetch").description("Fetch a ca
|
|
|
326
1328
|
paymentProtocol: paymentMeta.protocol,
|
|
327
1329
|
paymentChain: paymentMeta.chain,
|
|
328
1330
|
paymentTxHash: paymentMeta.txHash ?? void 0,
|
|
329
|
-
paymentMode: "charge"
|
|
1331
|
+
paymentMode: sessionMeta ? "session" : "charge"
|
|
330
1332
|
}
|
|
331
1333
|
});
|
|
332
1334
|
runId = runResult.runId;
|
|
@@ -391,7 +1393,7 @@ var fetchCommand = (appContext) => new Command2("fetch").description("Fetch a ca
|
|
|
391
1393
|
);
|
|
392
1394
|
|
|
393
1395
|
// src/commands/get-command.ts
|
|
394
|
-
import { Command as
|
|
1396
|
+
import { Command as Command4 } from "commander";
|
|
395
1397
|
var formatReviewCount = (count) => {
|
|
396
1398
|
if (count >= 1e3) return `${(count / 1e3).toFixed(1)}k`;
|
|
397
1399
|
return count.toString();
|
|
@@ -446,7 +1448,7 @@ var formatCapability = (capability) => {
|
|
|
446
1448
|
lines.push(` Method: ${capability.method}`);
|
|
447
1449
|
return lines.join("\n");
|
|
448
1450
|
};
|
|
449
|
-
var getCommand = (appContext) => new
|
|
1451
|
+
var getCommand = (appContext) => new Command4("get").description(
|
|
450
1452
|
"Get details for a capability by position from last search, or by slug"
|
|
451
1453
|
).argument(
|
|
452
1454
|
"<identifier>",
|
|
@@ -500,20 +1502,21 @@ var getCommand = (appContext) => new Command3("get").description(
|
|
|
500
1502
|
});
|
|
501
1503
|
|
|
502
1504
|
// src/commands/init-command.ts
|
|
503
|
-
import { createHash } from "crypto";
|
|
1505
|
+
import { createHash as createHash3 } from "crypto";
|
|
504
1506
|
import {
|
|
505
1507
|
chmodSync,
|
|
506
1508
|
cpSync,
|
|
507
1509
|
existsSync as existsSync2,
|
|
508
1510
|
mkdirSync as mkdirSync2,
|
|
509
1511
|
readdirSync,
|
|
510
|
-
readFileSync as
|
|
1512
|
+
readFileSync as readFileSync3,
|
|
1513
|
+
rmSync,
|
|
511
1514
|
writeFileSync as writeFileSync2
|
|
512
1515
|
} from "fs";
|
|
513
1516
|
import { homedir as homedir2 } from "os";
|
|
514
1517
|
import { dirname, join as join2, relative } from "path";
|
|
515
1518
|
import { fileURLToPath } from "url";
|
|
516
|
-
import { Command as
|
|
1519
|
+
import { Command as Command5 } from "commander";
|
|
517
1520
|
import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
|
|
518
1521
|
var AGENT_TOOLS = [
|
|
519
1522
|
{ name: "Claude Code", configDir: ".claude" },
|
|
@@ -535,7 +1538,7 @@ var getPackageRoot = () => {
|
|
|
535
1538
|
}
|
|
536
1539
|
return dir;
|
|
537
1540
|
};
|
|
538
|
-
var sha256File = (filePath) =>
|
|
1541
|
+
var sha256File = (filePath) => createHash3("sha256").update(readFileSync3(filePath)).digest("hex");
|
|
539
1542
|
var verifyFileCopy = (src, dest) => {
|
|
540
1543
|
if (!existsSync2(dest)) return false;
|
|
541
1544
|
return sha256File(src) === sha256File(dest);
|
|
@@ -577,7 +1580,7 @@ var installHook = (home) => {
|
|
|
577
1580
|
let settings = {};
|
|
578
1581
|
if (existsSync2(settingsPath)) {
|
|
579
1582
|
try {
|
|
580
|
-
settings = JSON.parse(
|
|
1583
|
+
settings = JSON.parse(readFileSync3(settingsPath, "utf-8"));
|
|
581
1584
|
} catch {
|
|
582
1585
|
}
|
|
583
1586
|
}
|
|
@@ -654,6 +1657,36 @@ var installHook = (home) => {
|
|
|
654
1657
|
`);
|
|
655
1658
|
return true;
|
|
656
1659
|
};
|
|
1660
|
+
var CONFLICTING_SKILL_PATTERNS = ["zam", "tempo"];
|
|
1661
|
+
var findConflictingSkills = (home) => {
|
|
1662
|
+
const found = [];
|
|
1663
|
+
for (const tool of AGENT_TOOLS) {
|
|
1664
|
+
const toolSkillsPath = join2(home, tool.configDir, "skills");
|
|
1665
|
+
if (!existsSync2(toolSkillsPath)) continue;
|
|
1666
|
+
const entries = readdirSync(toolSkillsPath, { withFileTypes: true });
|
|
1667
|
+
for (const entry of entries) {
|
|
1668
|
+
if (!entry.isDirectory()) continue;
|
|
1669
|
+
const lower = entry.name.toLowerCase();
|
|
1670
|
+
if (CONFLICTING_SKILL_PATTERNS.some((p) => lower.includes(p))) {
|
|
1671
|
+
found.push({
|
|
1672
|
+
tool: tool.name,
|
|
1673
|
+
skillPath: join2(toolSkillsPath, entry.name),
|
|
1674
|
+
skillName: entry.name
|
|
1675
|
+
});
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
return found;
|
|
1680
|
+
};
|
|
1681
|
+
var removeConflictingSkills = (home) => {
|
|
1682
|
+
const conflicts = findConflictingSkills(home);
|
|
1683
|
+
const removed = [];
|
|
1684
|
+
for (const { skillPath, tool, skillName } of conflicts) {
|
|
1685
|
+
rmSync(skillPath, { recursive: true, force: true });
|
|
1686
|
+
removed.push(`${tool}: ${skillName}`);
|
|
1687
|
+
}
|
|
1688
|
+
return removed;
|
|
1689
|
+
};
|
|
657
1690
|
var installSkills = (home) => {
|
|
658
1691
|
const skillsSourceDir = join2(getPackageRoot(), "skills");
|
|
659
1692
|
const skillDirs = readdirSync(skillsSourceDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
@@ -683,7 +1716,7 @@ var installSkills = (home) => {
|
|
|
683
1716
|
}
|
|
684
1717
|
return installed;
|
|
685
1718
|
};
|
|
686
|
-
var initCommand = (appContext) => new
|
|
1719
|
+
var initCommand = (appContext) => new Command5("init").description("Initialize Zero CLI for usage").option("--force", "Overwrite existing configuration").action(async (options) => {
|
|
687
1720
|
const home = homedir2();
|
|
688
1721
|
const zeroDir = join2(home, ".zero");
|
|
689
1722
|
const configPath = join2(zeroDir, "config.json");
|
|
@@ -692,7 +1725,7 @@ var initCommand = (appContext) => new Command4("init").description("Initialize Z
|
|
|
692
1725
|
const walletExists = (() => {
|
|
693
1726
|
if (!existsSync2(configPath)) return false;
|
|
694
1727
|
try {
|
|
695
|
-
const existing = JSON.parse(
|
|
1728
|
+
const existing = JSON.parse(readFileSync3(configPath, "utf8"));
|
|
696
1729
|
return !!existing.privateKey;
|
|
697
1730
|
} catch {
|
|
698
1731
|
return false;
|
|
@@ -702,7 +1735,7 @@ var initCommand = (appContext) => new Command4("init").description("Initialize Z
|
|
|
702
1735
|
const privateKey = generatePrivateKey();
|
|
703
1736
|
const account = privateKeyToAccount(privateKey);
|
|
704
1737
|
mkdirSync2(zeroDir, { recursive: true });
|
|
705
|
-
const existing = existsSync2(configPath) ? JSON.parse(
|
|
1738
|
+
const existing = existsSync2(configPath) ? JSON.parse(readFileSync3(configPath, "utf8")) : {};
|
|
706
1739
|
writeFileSync2(
|
|
707
1740
|
configPath,
|
|
708
1741
|
JSON.stringify(
|
|
@@ -716,7 +1749,7 @@ var initCommand = (appContext) => new Command4("init").description("Initialize Z
|
|
|
716
1749
|
console.log(`Wallet address: ${account.address}`);
|
|
717
1750
|
} else {
|
|
718
1751
|
try {
|
|
719
|
-
const existing = JSON.parse(
|
|
1752
|
+
const existing = JSON.parse(readFileSync3(configPath, "utf8"));
|
|
720
1753
|
const account = privateKeyToAccount(existing.privateKey);
|
|
721
1754
|
walletAddress = account.address;
|
|
722
1755
|
} catch {
|
|
@@ -748,8 +1781,19 @@ var initCommand = (appContext) => new Command4("init").description("Initialize Z
|
|
|
748
1781
|
} catch (err) {
|
|
749
1782
|
hookError = err instanceof Error ? err.message : "unknown hook error";
|
|
750
1783
|
}
|
|
1784
|
+
const conflictingSkills = findConflictingSkills(home);
|
|
1785
|
+
if (conflictingSkills.length > 0) {
|
|
1786
|
+
const skillList = conflictingSkills.map((s) => ` - ${s.tool}: ${s.skillName}`).join("\n");
|
|
1787
|
+
console.error(
|
|
1788
|
+
`
|
|
1789
|
+
Found deprecated skills that may conflict with Zero:
|
|
1790
|
+
${skillList}
|
|
1791
|
+
|
|
1792
|
+
To remove them, run: zero init cleanup`
|
|
1793
|
+
);
|
|
1794
|
+
}
|
|
751
1795
|
console.error(
|
|
752
|
-
'Zero is ready! Run `zero search` to find capabilities.\n\nTry:\n zero search "translate text to Spanish"\n zero search "generate an image"\n zero search "weather forecast"'
|
|
1796
|
+
'Zero is ready! Run `zero search` to find capabilities.\n\nBy using Zero, you agree to our Terms of Service:\n https://zero.xyz/terms-of-service\n\nRun `zero terms` to view the full terms.\n\nTry:\n zero search "translate text to Spanish"\n zero search "generate an image"\n zero search "weather forecast"'
|
|
753
1797
|
);
|
|
754
1798
|
appContext.services.analyticsService.capture("wallet_initialized", {
|
|
755
1799
|
// biome-ignore lint/style/useNamingConvention: snake_case for analytics
|
|
@@ -770,23 +1814,45 @@ var initCommand = (appContext) => new Command4("init").description("Initialize Z
|
|
|
770
1814
|
hook_installed: hookInstalled,
|
|
771
1815
|
// biome-ignore lint/style/useNamingConvention: snake_case for analytics
|
|
772
1816
|
hook_error: hookError,
|
|
1817
|
+
// biome-ignore lint/style/useNamingConvention: snake_case for analytics
|
|
1818
|
+
conflicting_skills_found: conflictingSkills.length,
|
|
773
1819
|
force: options.force ?? false
|
|
774
1820
|
});
|
|
775
|
-
})
|
|
1821
|
+
}).addCommand(
|
|
1822
|
+
new Command5("cleanup").description(
|
|
1823
|
+
"Remove deprecated skills (zam, tempo) that conflict with Zero"
|
|
1824
|
+
).action(() => {
|
|
1825
|
+
const home = homedir2();
|
|
1826
|
+
const removed = removeConflictingSkills(home);
|
|
1827
|
+
if (removed.length === 0) {
|
|
1828
|
+
console.error("No conflicting skills found. Nothing to remove.");
|
|
1829
|
+
return;
|
|
1830
|
+
}
|
|
1831
|
+
const removedList = removed.map((s) => ` - ${s}`).join("\n");
|
|
1832
|
+
console.error(`Removed deprecated skills:
|
|
1833
|
+
${removedList}`);
|
|
1834
|
+
appContext.services.analyticsService.capture("skills_cleanup", {
|
|
1835
|
+
// biome-ignore lint/style/useNamingConvention: snake_case for analytics
|
|
1836
|
+
skills_removed: removed,
|
|
1837
|
+
// biome-ignore lint/style/useNamingConvention: snake_case for analytics
|
|
1838
|
+
skills_removed_count: removed.length
|
|
1839
|
+
});
|
|
1840
|
+
})
|
|
1841
|
+
);
|
|
776
1842
|
|
|
777
1843
|
// src/commands/review-command.ts
|
|
778
|
-
import { readFileSync as
|
|
779
|
-
import { Command as
|
|
780
|
-
import { z } from "zod";
|
|
781
|
-
var
|
|
782
|
-
runId:
|
|
783
|
-
success:
|
|
784
|
-
accuracy:
|
|
785
|
-
value:
|
|
786
|
-
reliability:
|
|
787
|
-
content:
|
|
1844
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
1845
|
+
import { Command as Command6 } from "commander";
|
|
1846
|
+
import { z as z3 } from "zod";
|
|
1847
|
+
var bulkEntrySchema2 = z3.object({
|
|
1848
|
+
runId: z3.string(),
|
|
1849
|
+
success: z3.boolean(),
|
|
1850
|
+
accuracy: z3.number().int().min(1).max(5),
|
|
1851
|
+
value: z3.number().int().min(1).max(5),
|
|
1852
|
+
reliability: z3.number().int().min(1).max(5),
|
|
1853
|
+
content: z3.string().optional()
|
|
788
1854
|
});
|
|
789
|
-
var reviewCommand = (appContext) => new
|
|
1855
|
+
var reviewCommand = (appContext) => new Command6("review").description("Submit a review for a capability run").addHelpText(
|
|
790
1856
|
"after",
|
|
791
1857
|
`
|
|
792
1858
|
Tips for a great review:
|
|
@@ -814,14 +1880,14 @@ Examples:
|
|
|
814
1880
|
try {
|
|
815
1881
|
const { analyticsService, apiService } = appContext.services;
|
|
816
1882
|
if (options.fromFile) {
|
|
817
|
-
const contents =
|
|
1883
|
+
const contents = readFileSync4(options.fromFile, "utf8");
|
|
818
1884
|
const lines = contents.split("\n").map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
|
|
819
1885
|
let ok = 0;
|
|
820
1886
|
let failed = 0;
|
|
821
1887
|
for (const [idx, line] of lines.entries()) {
|
|
822
1888
|
const lineNum = idx + 1;
|
|
823
1889
|
try {
|
|
824
|
-
const parsed =
|
|
1890
|
+
const parsed = bulkEntrySchema2.parse(JSON.parse(line));
|
|
825
1891
|
const result2 = await apiService.createReview(parsed);
|
|
826
1892
|
ok += 1;
|
|
827
1893
|
console.log(
|
|
@@ -922,8 +1988,8 @@ Bulk review complete: ${ok} ok, ${failed} failed`);
|
|
|
922
1988
|
);
|
|
923
1989
|
|
|
924
1990
|
// src/commands/runs-command.ts
|
|
925
|
-
import { Command as
|
|
926
|
-
var runsCommand = (appContext) => new
|
|
1991
|
+
import { Command as Command7 } from "commander";
|
|
1992
|
+
var runsCommand = (appContext) => new Command7("runs").description("List your recent capability runs").addHelpText(
|
|
927
1993
|
"after",
|
|
928
1994
|
`
|
|
929
1995
|
View your recent capability runs \u2014 status, latency, cost, and payment info.
|
|
@@ -971,7 +2037,7 @@ Examples:
|
|
|
971
2037
|
);
|
|
972
2038
|
|
|
973
2039
|
// src/commands/search-command.ts
|
|
974
|
-
import { Command as
|
|
2040
|
+
import { Command as Command8 } from "commander";
|
|
975
2041
|
var formatReviewCount2 = (count) => {
|
|
976
2042
|
if (count >= 1e3) return `${(count / 1e3).toFixed(1)}k`;
|
|
977
2043
|
return count.toString();
|
|
@@ -1009,7 +2075,7 @@ var formatSearchResults = (results) => {
|
|
|
1009
2075
|
"${displayDescription}"`;
|
|
1010
2076
|
}).join("\n");
|
|
1011
2077
|
};
|
|
1012
|
-
var searchCommand = (appContext) => new
|
|
2078
|
+
var searchCommand = (appContext) => new Command8("search").description("Search for capabilities").argument("<query>", "Search query").option("--json", "Output raw JSON to stdout").option("--offset <n>", "Pagination offset", Number).option("--limit <n>", "Results per page", Number).option("--free", "Only show free capabilities").option("--max-cost <amount>", "Maximum cost per call").option("--min-rating <stars>", "Minimum star rating (1-5)", Number).option("--protocol <protocol>", "Payment protocol (x402 or mpp)").option("--min-trust <n>", "Minimum trust score (0-100)", Number).option(
|
|
1013
2079
|
"--status <status>",
|
|
1014
2080
|
"Filter by availability (healthy, degraded, down)"
|
|
1015
2081
|
).option("--all", "Show all results (no trust or health filtering)").option(
|
|
@@ -1086,14 +2152,34 @@ var searchCommand = (appContext) => new Command7("search").description("Search f
|
|
|
1086
2152
|
}
|
|
1087
2153
|
);
|
|
1088
2154
|
|
|
2155
|
+
// src/commands/terms-command.ts
|
|
2156
|
+
import { Command as Command9 } from "commander";
|
|
2157
|
+
var TERMS_URL = "https://zero.xyz/terms-of-service";
|
|
2158
|
+
var termsCommand = (_appContext) => new Command9("terms").description("View the ZeroClick Terms of Service").action(async () => {
|
|
2159
|
+
console.log(
|
|
2160
|
+
`ZeroClick Agentic Capability Search \u2014 Terms of Service
|
|
2161
|
+
|
|
2162
|
+
By using Zero, you agree to our Terms of Service.
|
|
2163
|
+
|
|
2164
|
+
Read the full terms at: ${TERMS_URL}
|
|
2165
|
+
`
|
|
2166
|
+
);
|
|
2167
|
+
try {
|
|
2168
|
+
const { exec } = await import("child_process");
|
|
2169
|
+
const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
2170
|
+
exec(`${openCmd} ${TERMS_URL}`);
|
|
2171
|
+
} catch {
|
|
2172
|
+
}
|
|
2173
|
+
});
|
|
2174
|
+
|
|
1089
2175
|
// src/commands/wallet-command.ts
|
|
1090
|
-
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as
|
|
2176
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
|
|
1091
2177
|
import { homedir as homedir3 } from "os";
|
|
1092
2178
|
import { join as join3 } from "path";
|
|
1093
|
-
import { Command as
|
|
2179
|
+
import { Command as Command10 } from "commander";
|
|
1094
2180
|
import open from "open";
|
|
1095
2181
|
import { privateKeyToAccount as privateKeyToAccount2 } from "viem/accounts";
|
|
1096
|
-
var walletBalanceCommand = (appContext) => new
|
|
2182
|
+
var walletBalanceCommand = (appContext) => new Command10("balance").description("Show wallet balance").action(async () => {
|
|
1097
2183
|
const { walletService } = appContext.services;
|
|
1098
2184
|
const balance = await walletService.getBalance();
|
|
1099
2185
|
if (balance === null) {
|
|
@@ -1108,7 +2194,11 @@ var walletBalanceCommand = (appContext) => new Command8("balance").description("
|
|
|
1108
2194
|
}
|
|
1109
2195
|
console.log(`${balance.amount} ${balance.asset}`);
|
|
1110
2196
|
});
|
|
1111
|
-
var walletFundCommand = (appContext) => new
|
|
2197
|
+
var walletFundCommand = (appContext) => new Command10("fund").description("Fund your wallet").argument("[amount]", "Amount to fund in USDC").option("--manual", "Show wallet address for manual transfer").option(
|
|
2198
|
+
"--use <provider>",
|
|
2199
|
+
"Onramp provider: coinbase or stripe",
|
|
2200
|
+
"coinbase"
|
|
2201
|
+
).action(
|
|
1112
2202
|
async (amount, options) => {
|
|
1113
2203
|
const { analyticsService, walletService } = appContext.services;
|
|
1114
2204
|
const address = walletService.getAddress();
|
|
@@ -1126,7 +2216,11 @@ ${address}`);
|
|
|
1126
2216
|
});
|
|
1127
2217
|
return;
|
|
1128
2218
|
}
|
|
1129
|
-
const
|
|
2219
|
+
const provider = options.use === "stripe" ? "stripe" : "coinbase";
|
|
2220
|
+
const url = await appContext.services.apiService.getFundingUrl(
|
|
2221
|
+
amount,
|
|
2222
|
+
provider
|
|
2223
|
+
);
|
|
1130
2224
|
if (url) {
|
|
1131
2225
|
await open(url);
|
|
1132
2226
|
console.log("Opened funding page in your browser.");
|
|
@@ -1145,7 +2239,7 @@ ${address}`);
|
|
|
1145
2239
|
}
|
|
1146
2240
|
}
|
|
1147
2241
|
);
|
|
1148
|
-
var walletAddressCommand = (appContext) => new
|
|
2242
|
+
var walletAddressCommand = (appContext) => new Command10("address").description("Show wallet address").action(() => {
|
|
1149
2243
|
const { walletService } = appContext.services;
|
|
1150
2244
|
const address = walletService.getAddress();
|
|
1151
2245
|
if (!address) {
|
|
@@ -1155,7 +2249,7 @@ var walletAddressCommand = (appContext) => new Command8("address").description("
|
|
|
1155
2249
|
}
|
|
1156
2250
|
console.log(address);
|
|
1157
2251
|
});
|
|
1158
|
-
var walletSetCommand = (appContext) => new
|
|
2252
|
+
var walletSetCommand = (appContext) => new Command10("set").description("Set wallet from an existing private key").argument("<privateKey>", "Hex-encoded private key (0x-prefixed)").option("--force", "Overwrite existing wallet without prompting").action(async (privateKey, options) => {
|
|
1159
2253
|
const { analyticsService } = appContext.services;
|
|
1160
2254
|
if (!privateKey.startsWith("0x")) {
|
|
1161
2255
|
console.error("Private key must be 0x-prefixed hex string.");
|
|
@@ -1174,7 +2268,7 @@ var walletSetCommand = (appContext) => new Command8("set").description("Set wall
|
|
|
1174
2268
|
const configPath = join3(zeroDir, "config.json");
|
|
1175
2269
|
if (!options.force && existsSync3(configPath)) {
|
|
1176
2270
|
try {
|
|
1177
|
-
const existing2 = JSON.parse(
|
|
2271
|
+
const existing2 = JSON.parse(readFileSync5(configPath, "utf8"));
|
|
1178
2272
|
if (existing2.privateKey) {
|
|
1179
2273
|
console.error(
|
|
1180
2274
|
"Wallet already configured. Use --force to overwrite."
|
|
@@ -1186,7 +2280,7 @@ var walletSetCommand = (appContext) => new Command8("set").description("Set wall
|
|
|
1186
2280
|
}
|
|
1187
2281
|
}
|
|
1188
2282
|
mkdirSync3(zeroDir, { recursive: true });
|
|
1189
|
-
const existing = existsSync3(configPath) ? JSON.parse(
|
|
2283
|
+
const existing = existsSync3(configPath) ? JSON.parse(readFileSync5(configPath, "utf8")) : {};
|
|
1190
2284
|
writeFileSync3(
|
|
1191
2285
|
configPath,
|
|
1192
2286
|
JSON.stringify(
|
|
@@ -1207,7 +2301,7 @@ var walletSetCommand = (appContext) => new Command8("set").description("Set wall
|
|
|
1207
2301
|
});
|
|
1208
2302
|
});
|
|
1209
2303
|
var walletCommand = (appContext) => {
|
|
1210
|
-
const cmd = new
|
|
2304
|
+
const cmd = new Command10("wallet").description("Manage your wallet");
|
|
1211
2305
|
cmd.addCommand(walletBalanceCommand(appContext));
|
|
1212
2306
|
cmd.addCommand(walletFundCommand(appContext));
|
|
1213
2307
|
cmd.addCommand(walletAddressCommand(appContext));
|
|
@@ -1218,7 +2312,7 @@ var walletCommand = (appContext) => {
|
|
|
1218
2312
|
// src/app.ts
|
|
1219
2313
|
var createApp = (appContext) => {
|
|
1220
2314
|
const { analyticsService } = appContext.services;
|
|
1221
|
-
const program = new
|
|
2315
|
+
const program = new Command11().name("zero").description("Zero CLI \u2014 Search engine and payment platform for AI agents").version(package_default.version, "-v, --version").exitOverride().hook("preAction", async (_thisCommand, actionCommand) => {
|
|
1222
2316
|
analyticsService.capture("command_executed", {
|
|
1223
2317
|
command: actionCommand.name()
|
|
1224
2318
|
});
|
|
@@ -1229,16 +2323,18 @@ var createApp = (appContext) => {
|
|
|
1229
2323
|
program.addCommand(fetchCommand(appContext));
|
|
1230
2324
|
program.addCommand(reviewCommand(appContext));
|
|
1231
2325
|
program.addCommand(runsCommand(appContext));
|
|
2326
|
+
program.addCommand(bugReportCommand(appContext));
|
|
1232
2327
|
program.addCommand(walletCommand(appContext));
|
|
1233
2328
|
program.addCommand(configCommand(appContext));
|
|
2329
|
+
program.addCommand(termsCommand(appContext));
|
|
1234
2330
|
return program;
|
|
1235
2331
|
};
|
|
1236
2332
|
|
|
1237
2333
|
// src/app/app-env.ts
|
|
1238
|
-
import
|
|
1239
|
-
var envSchema =
|
|
1240
|
-
ZERO_API_URL:
|
|
1241
|
-
ZERO_PRIVATE_KEY:
|
|
2334
|
+
import z4 from "zod";
|
|
2335
|
+
var envSchema = z4.object({
|
|
2336
|
+
ZERO_API_URL: z4.string().default("https://api.zero.xyz"),
|
|
2337
|
+
ZERO_PRIVATE_KEY: z4.string().optional()
|
|
1242
2338
|
});
|
|
1243
2339
|
var getEnv = () => {
|
|
1244
2340
|
try {
|
|
@@ -1251,14 +2347,14 @@ var getEnv = () => {
|
|
|
1251
2347
|
};
|
|
1252
2348
|
|
|
1253
2349
|
// src/app/app-services.ts
|
|
1254
|
-
import { existsSync as existsSync6, readFileSync as
|
|
2350
|
+
import { existsSync as existsSync6, readFileSync as readFileSync8 } from "fs";
|
|
1255
2351
|
import { homedir as homedir4 } from "os";
|
|
1256
2352
|
import { join as join5 } from "path";
|
|
1257
2353
|
import { privateKeyToAccount as privateKeyToAccount3 } from "viem/accounts";
|
|
1258
2354
|
|
|
1259
2355
|
// src/services/analytics-service.ts
|
|
1260
2356
|
import { randomUUID } from "crypto";
|
|
1261
|
-
import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as
|
|
2357
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
|
|
1262
2358
|
import { dirname as dirname2 } from "path";
|
|
1263
2359
|
import { PostHog } from "posthog-node";
|
|
1264
2360
|
var POSTHOG_API_KEY = "phc_B2vLyNxAf2mnqvdPQajf4d4b2iXc35dep2ZrvebMJLuX";
|
|
@@ -1273,7 +2369,7 @@ var AnalyticsService = class {
|
|
|
1273
2369
|
let persistedAnonId;
|
|
1274
2370
|
try {
|
|
1275
2371
|
if (existsSync4(opts.configPath)) {
|
|
1276
|
-
const config = JSON.parse(
|
|
2372
|
+
const config = JSON.parse(readFileSync6(opts.configPath, "utf8"));
|
|
1277
2373
|
if (config.telemetry === false) {
|
|
1278
2374
|
telemetryEnabled = false;
|
|
1279
2375
|
}
|
|
@@ -1298,7 +2394,7 @@ var AnalyticsService = class {
|
|
|
1298
2394
|
try {
|
|
1299
2395
|
const dir = dirname2(opts.configPath);
|
|
1300
2396
|
mkdirSync4(dir, { recursive: true });
|
|
1301
|
-
const existing = existsSync4(opts.configPath) ? JSON.parse(
|
|
2397
|
+
const existing = existsSync4(opts.configPath) ? JSON.parse(readFileSync6(opts.configPath, "utf8")) : {};
|
|
1302
2398
|
writeFileSync4(
|
|
1303
2399
|
opts.configPath,
|
|
1304
2400
|
JSON.stringify({ ...existing, anonId: newAnonId }, null, 2)
|
|
@@ -1344,510 +2440,8 @@ var AnalyticsService = class {
|
|
|
1344
2440
|
}
|
|
1345
2441
|
};
|
|
1346
2442
|
|
|
1347
|
-
// src/services/api-service.ts
|
|
1348
|
-
import { createHash as createHash2 } from "crypto";
|
|
1349
|
-
import z3 from "zod";
|
|
1350
|
-
var searchResultSchema = z3.object({
|
|
1351
|
-
id: z3.string(),
|
|
1352
|
-
position: z3.number(),
|
|
1353
|
-
slug: z3.string(),
|
|
1354
|
-
name: z3.string(),
|
|
1355
|
-
canonicalName: z3.string().nullable().optional(),
|
|
1356
|
-
description: z3.string(),
|
|
1357
|
-
whatItDoes: z3.string().nullable().optional(),
|
|
1358
|
-
url: z3.string(),
|
|
1359
|
-
cost: z3.object({ amount: z3.string(), asset: z3.string() }),
|
|
1360
|
-
rating: z3.object({
|
|
1361
|
-
score: z3.string(),
|
|
1362
|
-
successRate: z3.string(),
|
|
1363
|
-
reviews: z3.number(),
|
|
1364
|
-
stars: z3.string().nullable().optional(),
|
|
1365
|
-
state: z3.enum(["unrated", "rated"]).optional()
|
|
1366
|
-
}),
|
|
1367
|
-
trustScore: z3.number().nullable().optional(),
|
|
1368
|
-
trustSignalCount: z3.number().optional(),
|
|
1369
|
-
availabilityStatus: z3.enum(["healthy", "degraded", "down", "unknown"]).nullable().optional(),
|
|
1370
|
-
relevanceScore: z3.number().optional()
|
|
1371
|
-
});
|
|
1372
|
-
var searchResponseSchema = z3.object({
|
|
1373
|
-
searchId: z3.string(),
|
|
1374
|
-
total: z3.number().optional().default(0),
|
|
1375
|
-
offset: z3.number().optional().default(0),
|
|
1376
|
-
hasMore: z3.boolean().optional().default(false),
|
|
1377
|
-
capabilities: z3.array(searchResultSchema)
|
|
1378
|
-
});
|
|
1379
|
-
var capabilityResponseSchema = z3.object({
|
|
1380
|
-
uid: z3.string(),
|
|
1381
|
-
slug: z3.string(),
|
|
1382
|
-
name: z3.string(),
|
|
1383
|
-
description: z3.string(),
|
|
1384
|
-
url: z3.string(),
|
|
1385
|
-
method: z3.string(),
|
|
1386
|
-
headers: z3.record(z3.string(), z3.string()).nullable(),
|
|
1387
|
-
bodySchema: z3.record(z3.string(), z3.unknown()).nullable(),
|
|
1388
|
-
responseSchema: z3.record(z3.string(), z3.unknown()).nullable(),
|
|
1389
|
-
example: z3.object({ request: z3.unknown(), response: z3.unknown() }).nullable(),
|
|
1390
|
-
tags: z3.array(z3.string()).nullable(),
|
|
1391
|
-
displayCostAmount: z3.string(),
|
|
1392
|
-
displayCostAsset: z3.string(),
|
|
1393
|
-
rating: z3.object({
|
|
1394
|
-
score: z3.string(),
|
|
1395
|
-
successRate: z3.string(),
|
|
1396
|
-
reviews: z3.number(),
|
|
1397
|
-
stars: z3.string().nullable().optional(),
|
|
1398
|
-
state: z3.enum(["unrated", "rated"]).optional()
|
|
1399
|
-
}),
|
|
1400
|
-
priceObserved: z3.object({
|
|
1401
|
-
medianCents: z3.string(),
|
|
1402
|
-
p95Cents: z3.string(),
|
|
1403
|
-
sampleCount: z3.number(),
|
|
1404
|
-
varies: z3.boolean()
|
|
1405
|
-
}).nullable().optional(),
|
|
1406
|
-
paymentMethods: z3.array(
|
|
1407
|
-
z3.object({
|
|
1408
|
-
uid: z3.string(),
|
|
1409
|
-
protocol: z3.string(),
|
|
1410
|
-
methodType: z3.string(),
|
|
1411
|
-
chain: z3.string().nullable(),
|
|
1412
|
-
mode: z3.string(),
|
|
1413
|
-
costAmount: z3.string(),
|
|
1414
|
-
costPer: z3.string(),
|
|
1415
|
-
priority: z3.number()
|
|
1416
|
-
})
|
|
1417
|
-
).nullable(),
|
|
1418
|
-
trustScore: z3.number().nullable().optional(),
|
|
1419
|
-
trustComponents: z3.object({
|
|
1420
|
-
apiQuality: z3.number().nullable(),
|
|
1421
|
-
blockchainActivity: z3.number().nullable(),
|
|
1422
|
-
performance: z3.number().nullable()
|
|
1423
|
-
}).nullable().optional(),
|
|
1424
|
-
availabilityStatus: z3.enum(["healthy", "degraded", "down", "unknown"]).nullable().optional()
|
|
1425
|
-
});
|
|
1426
|
-
var createRunResponseSchema = z3.object({
|
|
1427
|
-
runId: z3.string()
|
|
1428
|
-
});
|
|
1429
|
-
var createReviewResponseSchema = z3.object({
|
|
1430
|
-
reviewId: z3.string(),
|
|
1431
|
-
recorded: z3.boolean()
|
|
1432
|
-
});
|
|
1433
|
-
var runListItemSchema = z3.object({
|
|
1434
|
-
uid: z3.string(),
|
|
1435
|
-
capabilityUid: z3.string(),
|
|
1436
|
-
capabilitySlug: z3.string(),
|
|
1437
|
-
capabilityName: z3.string(),
|
|
1438
|
-
status: z3.number().nullable(),
|
|
1439
|
-
latencyMs: z3.number().nullable(),
|
|
1440
|
-
cost: z3.object({ amount: z3.string(), asset: z3.string().nullable() }).nullable(),
|
|
1441
|
-
payment: z3.object({
|
|
1442
|
-
protocol: z3.string(),
|
|
1443
|
-
chain: z3.string().nullable(),
|
|
1444
|
-
txHash: z3.string().nullable(),
|
|
1445
|
-
mode: z3.string().nullable()
|
|
1446
|
-
}).nullable(),
|
|
1447
|
-
createdAt: z3.coerce.date(),
|
|
1448
|
-
reviewed: z3.boolean()
|
|
1449
|
-
});
|
|
1450
|
-
var listRunsResponseSchema = z3.object({
|
|
1451
|
-
runs: z3.array(runListItemSchema),
|
|
1452
|
-
nextCursor: z3.string().nullable()
|
|
1453
|
-
});
|
|
1454
|
-
var buildCanonicalMessage = (method, path, body, timestamp, nonce) => {
|
|
1455
|
-
const bodyHash = createHash2("sha256").update(body ?? "").digest("hex");
|
|
1456
|
-
return `${method}:${path}:${bodyHash}:${timestamp}:${nonce}`;
|
|
1457
|
-
};
|
|
1458
|
-
var ApiService = class {
|
|
1459
|
-
constructor(baseUrl, account) {
|
|
1460
|
-
this.baseUrl = baseUrl;
|
|
1461
|
-
this.account = account;
|
|
1462
|
-
this.walletAddress = this.account?.address ?? null;
|
|
1463
|
-
}
|
|
1464
|
-
walletAddress;
|
|
1465
|
-
account;
|
|
1466
|
-
signRequest = async (method, path, body) => {
|
|
1467
|
-
if (!this.account) throw new Error("No private key configured");
|
|
1468
|
-
const timestamp = Math.floor(Date.now() / 1e3).toString();
|
|
1469
|
-
const nonce = crypto.randomUUID();
|
|
1470
|
-
const message = buildCanonicalMessage(method, path, body, timestamp, nonce);
|
|
1471
|
-
const signature = await this.account.signMessage({ message });
|
|
1472
|
-
return {
|
|
1473
|
-
"x-zero-address": this.account.address,
|
|
1474
|
-
"x-zero-timestamp": timestamp,
|
|
1475
|
-
"x-zero-nonce": nonce,
|
|
1476
|
-
"x-zero-signature": signature
|
|
1477
|
-
};
|
|
1478
|
-
};
|
|
1479
|
-
request = async (method, path, body) => {
|
|
1480
|
-
const url = `${this.baseUrl}${path}`;
|
|
1481
|
-
const bodyStr = body ? JSON.stringify(body) : void 0;
|
|
1482
|
-
const headers = {
|
|
1483
|
-
"content-type": "application/json"
|
|
1484
|
-
};
|
|
1485
|
-
if (this.account) {
|
|
1486
|
-
const walletHeaders = await this.signRequest(method, path, bodyStr);
|
|
1487
|
-
Object.assign(headers, walletHeaders);
|
|
1488
|
-
}
|
|
1489
|
-
const response = await fetch(url, {
|
|
1490
|
-
method,
|
|
1491
|
-
headers,
|
|
1492
|
-
body: bodyStr
|
|
1493
|
-
});
|
|
1494
|
-
if (!response.ok) {
|
|
1495
|
-
const errorBody = await response.json().catch(() => ({ error: "Unknown error" }));
|
|
1496
|
-
throw new Error(
|
|
1497
|
-
errorBody.error ?? `HTTP ${response.status}`
|
|
1498
|
-
);
|
|
1499
|
-
}
|
|
1500
|
-
return response.json();
|
|
1501
|
-
};
|
|
1502
|
-
search = async (options) => {
|
|
1503
|
-
const json = await this.request("POST", "/v1/search", options);
|
|
1504
|
-
return searchResponseSchema.parse(json);
|
|
1505
|
-
};
|
|
1506
|
-
getCapability = async (id, searchId) => {
|
|
1507
|
-
const qs = searchId ? `?searchId=${encodeURIComponent(searchId)}` : "";
|
|
1508
|
-
const json = await this.request(
|
|
1509
|
-
"GET",
|
|
1510
|
-
`/v1/capabilities/${encodeURIComponent(id)}${qs}`
|
|
1511
|
-
);
|
|
1512
|
-
return capabilityResponseSchema.parse(json);
|
|
1513
|
-
};
|
|
1514
|
-
createRun = async (data) => {
|
|
1515
|
-
const json = await this.request("POST", "/v1/runs", data);
|
|
1516
|
-
return createRunResponseSchema.parse(json);
|
|
1517
|
-
};
|
|
1518
|
-
listRuns = async (params = {}) => {
|
|
1519
|
-
const qs = new URLSearchParams();
|
|
1520
|
-
if (params.capabilityId) qs.set("capabilityId", params.capabilityId);
|
|
1521
|
-
if (params.unreviewed) qs.set("unreviewed", "true");
|
|
1522
|
-
if (params.limit) qs.set("limit", String(params.limit));
|
|
1523
|
-
if (params.cursor) qs.set("cursor", params.cursor);
|
|
1524
|
-
const suffix = qs.toString() ? `?${qs.toString()}` : "";
|
|
1525
|
-
const json = await this.request("GET", `/v1/runs${suffix}`);
|
|
1526
|
-
return listRunsResponseSchema.parse(json);
|
|
1527
|
-
};
|
|
1528
|
-
createReview = async (data) => {
|
|
1529
|
-
const json = await this.request("POST", "/v1/reviews", data);
|
|
1530
|
-
return createReviewResponseSchema.parse(json);
|
|
1531
|
-
};
|
|
1532
|
-
getFundingUrl = async (amount) => {
|
|
1533
|
-
try {
|
|
1534
|
-
const qs = amount ? `?amount=${encodeURIComponent(amount)}` : "";
|
|
1535
|
-
const json = await this.request("GET", `/v1/wallet/fund-url${qs}`);
|
|
1536
|
-
const parsed = z3.object({ url: z3.string() }).parse(json);
|
|
1537
|
-
return parsed.url;
|
|
1538
|
-
} catch {
|
|
1539
|
-
return null;
|
|
1540
|
-
}
|
|
1541
|
-
};
|
|
1542
|
-
};
|
|
1543
|
-
|
|
1544
|
-
// src/services/payment-service.ts
|
|
1545
|
-
import {
|
|
1546
|
-
adaptViemWallet,
|
|
1547
|
-
convertViemChainToRelayChain,
|
|
1548
|
-
createClient as createRelayClient,
|
|
1549
|
-
getClient as getRelayClient,
|
|
1550
|
-
MAINNET_RELAY_API
|
|
1551
|
-
} from "@relayprotocol/relay-sdk";
|
|
1552
|
-
import { x402Client as X402Client } from "@x402/core/client";
|
|
1553
|
-
import { decodePaymentResponseHeader, x402HTTPClient } from "@x402/core/http";
|
|
1554
|
-
import { ExactEvmScheme } from "@x402/evm/exact/client";
|
|
1555
|
-
import { createSIWxClientHook } from "@x402/extensions/sign-in-with-x";
|
|
1556
|
-
import { wrapFetchWithPayment } from "@x402/fetch";
|
|
1557
|
-
import { Receipt } from "mppx";
|
|
1558
|
-
import { Mppx, tempo } from "mppx/client";
|
|
1559
|
-
import {
|
|
1560
|
-
createPublicClient,
|
|
1561
|
-
createWalletClient,
|
|
1562
|
-
formatUnits,
|
|
1563
|
-
http
|
|
1564
|
-
} from "viem";
|
|
1565
|
-
import { base, baseSepolia } from "viem/chains";
|
|
1566
|
-
var USDC_BASE = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913";
|
|
1567
|
-
var USDC_BASE_SEPOLIA = "0x036CbD53842c5426634e7929541eC2318f3dCF7e";
|
|
1568
|
-
var USDC_TEMPO = "0x20c000000000000000000000b9537d11c60e8b50";
|
|
1569
|
-
var PATHUSD_TEMPO = "0x20c0000000000000000000000000000000000000";
|
|
1570
|
-
var BASE_CHAIN_ID = 8453;
|
|
1571
|
-
var TEMPO_CHAIN_ID = 4217;
|
|
1572
|
-
var TEMPO_TESTNET_CHAIN_ID = 42431;
|
|
1573
|
-
var DEFAULT_MAX_DEPOSIT = "100";
|
|
1574
|
-
var KNOWN_EIP712_DOMAINS = {
|
|
1575
|
-
// USDC on Base
|
|
1576
|
-
[USDC_BASE.toLowerCase()]: { name: "USDC", version: "2" },
|
|
1577
|
-
// USDC on Base Sepolia
|
|
1578
|
-
[USDC_BASE_SEPOLIA.toLowerCase()]: { name: "USDC", version: "2" }
|
|
1579
|
-
};
|
|
1580
|
-
var buildRelayClientOptions = () => ({
|
|
1581
|
-
baseApiUrl: MAINNET_RELAY_API,
|
|
1582
|
-
source: "zero-cli",
|
|
1583
|
-
chains: [convertViemChainToRelayChain(base)]
|
|
1584
|
-
});
|
|
1585
|
-
var calculateBuffer = (baseBalance) => {
|
|
1586
|
-
const twentyFivePercent = baseBalance / 4n;
|
|
1587
|
-
const twoDollars = 2000000n;
|
|
1588
|
-
return twentyFivePercent < twoDollars ? twentyFivePercent : twoDollars;
|
|
1589
|
-
};
|
|
1590
|
-
var tempoChain = {
|
|
1591
|
-
id: TEMPO_CHAIN_ID,
|
|
1592
|
-
name: "Tempo",
|
|
1593
|
-
nativeCurrency: { name: "USD", symbol: "USD", decimals: 18 },
|
|
1594
|
-
rpcUrls: {
|
|
1595
|
-
default: { http: ["https://rpc.tempo.xyz"] }
|
|
1596
|
-
}
|
|
1597
|
-
};
|
|
1598
|
-
var tempoTestnetChain = {
|
|
1599
|
-
id: TEMPO_TESTNET_CHAIN_ID,
|
|
1600
|
-
name: "Tempo Testnet",
|
|
1601
|
-
nativeCurrency: { name: "USD", symbol: "USD", decimals: 18 },
|
|
1602
|
-
rpcUrls: {
|
|
1603
|
-
default: { http: ["https://rpc.moderato.tempo.xyz"] }
|
|
1604
|
-
}
|
|
1605
|
-
};
|
|
1606
|
-
var ERC20_BALANCE_ABI = [
|
|
1607
|
-
{
|
|
1608
|
-
inputs: [{ name: "account", type: "address" }],
|
|
1609
|
-
name: "balanceOf",
|
|
1610
|
-
outputs: [{ name: "", type: "uint256" }],
|
|
1611
|
-
stateMutability: "view",
|
|
1612
|
-
type: "function"
|
|
1613
|
-
}
|
|
1614
|
-
];
|
|
1615
|
-
var PaymentService = class {
|
|
1616
|
-
constructor(account, config) {
|
|
1617
|
-
this.account = account;
|
|
1618
|
-
this.config = config;
|
|
1619
|
-
}
|
|
1620
|
-
relayInitialized = false;
|
|
1621
|
-
ensureRelayClient = () => {
|
|
1622
|
-
if (!this.relayInitialized) {
|
|
1623
|
-
createRelayClient(buildRelayClientOptions());
|
|
1624
|
-
this.relayInitialized = true;
|
|
1625
|
-
}
|
|
1626
|
-
};
|
|
1627
|
-
bridgeToTempo = async (requiredAmount, onProgress) => {
|
|
1628
|
-
if (!this.account) throw new Error("No wallet configured");
|
|
1629
|
-
this.ensureRelayClient();
|
|
1630
|
-
const baseBalance = await this.getBalanceRaw("base");
|
|
1631
|
-
const buffer = calculateBuffer(baseBalance);
|
|
1632
|
-
const bridgeAmount = requiredAmount + buffer;
|
|
1633
|
-
if (baseBalance < bridgeAmount) {
|
|
1634
|
-
throw new Error(
|
|
1635
|
-
`Insufficient Base USDC to bridge: have ${formatUnits(baseBalance, 6)}, need ${formatUnits(bridgeAmount, 6)} (${formatUnits(requiredAmount, 6)} + ${formatUnits(buffer, 6)} buffer)`
|
|
1636
|
-
);
|
|
1637
|
-
}
|
|
1638
|
-
onProgress?.(
|
|
1639
|
-
`Bridging ${formatUnits(bridgeAmount, 6)} USDC from Base to Tempo...`
|
|
1640
|
-
);
|
|
1641
|
-
const walletClient = createWalletClient({
|
|
1642
|
-
account: this.account,
|
|
1643
|
-
chain: base,
|
|
1644
|
-
transport: http()
|
|
1645
|
-
});
|
|
1646
|
-
const quote = await getRelayClient().actions.getQuote({
|
|
1647
|
-
chainId: BASE_CHAIN_ID,
|
|
1648
|
-
toChainId: TEMPO_CHAIN_ID,
|
|
1649
|
-
currency: USDC_BASE,
|
|
1650
|
-
toCurrency: USDC_TEMPO,
|
|
1651
|
-
amount: bridgeAmount.toString(),
|
|
1652
|
-
tradeType: "EXACT_INPUT",
|
|
1653
|
-
user: this.account.address,
|
|
1654
|
-
recipient: this.account.address,
|
|
1655
|
-
options: {
|
|
1656
|
-
usePermit: true
|
|
1657
|
-
}
|
|
1658
|
-
});
|
|
1659
|
-
let bridgeTxHash = null;
|
|
1660
|
-
await getRelayClient().actions.execute({
|
|
1661
|
-
quote,
|
|
1662
|
-
wallet: adaptViemWallet(walletClient),
|
|
1663
|
-
onProgress: ({ txHashes }) => {
|
|
1664
|
-
if (txHashes?.length && !bridgeTxHash) {
|
|
1665
|
-
bridgeTxHash = txHashes[0]?.txHash ?? null;
|
|
1666
|
-
}
|
|
1667
|
-
}
|
|
1668
|
-
});
|
|
1669
|
-
return bridgeTxHash;
|
|
1670
|
-
};
|
|
1671
|
-
handlePayment = async (url, request, paymentRequirement, maxPay, onProgress) => {
|
|
1672
|
-
if (!this.account) {
|
|
1673
|
-
throw new Error(
|
|
1674
|
-
"No wallet configured \u2014 run `zero init` or set ZERO_PRIVATE_KEY"
|
|
1675
|
-
);
|
|
1676
|
-
}
|
|
1677
|
-
if (paymentRequirement.protocol === "x402") {
|
|
1678
|
-
onProgress?.("Paying via x402 on Base...");
|
|
1679
|
-
return this.payX402(url, request, paymentRequirement.raw, maxPay);
|
|
1680
|
-
}
|
|
1681
|
-
if (paymentRequirement.protocol === "mpp") {
|
|
1682
|
-
onProgress?.("Paying via MPP on Tempo...");
|
|
1683
|
-
return this.payMpp(
|
|
1684
|
-
url,
|
|
1685
|
-
request,
|
|
1686
|
-
paymentRequirement.raw,
|
|
1687
|
-
maxPay,
|
|
1688
|
-
onProgress
|
|
1689
|
-
);
|
|
1690
|
-
}
|
|
1691
|
-
throw new Error("Unrecognized 402 payment protocol");
|
|
1692
|
-
};
|
|
1693
|
-
payX402 = async (url, request, _raw, maxPay) => {
|
|
1694
|
-
if (!this.account) throw new Error("No wallet configured");
|
|
1695
|
-
let capturedAmount = "0";
|
|
1696
|
-
const client = new X402Client().register(
|
|
1697
|
-
"eip155:*",
|
|
1698
|
-
new ExactEvmScheme(this.account)
|
|
1699
|
-
);
|
|
1700
|
-
client.onBeforePaymentCreation(async (context) => {
|
|
1701
|
-
const selected = context.selectedRequirements;
|
|
1702
|
-
if (selected && (!selected.extra?.name || !selected.extra?.version)) {
|
|
1703
|
-
const known = KNOWN_EIP712_DOMAINS[selected.asset?.toLowerCase() ?? ""];
|
|
1704
|
-
if (known) {
|
|
1705
|
-
selected.extra = { ...selected.extra, ...known };
|
|
1706
|
-
}
|
|
1707
|
-
}
|
|
1708
|
-
const requirement = context.paymentRequired.accepts[0];
|
|
1709
|
-
if (!requirement) return;
|
|
1710
|
-
capturedAmount = formatUnits(BigInt(requirement.amount), 6);
|
|
1711
|
-
if (maxPay && Number.parseFloat(capturedAmount) > Number.parseFloat(maxPay)) {
|
|
1712
|
-
return {
|
|
1713
|
-
abort: true,
|
|
1714
|
-
reason: `Payment of ${capturedAmount} USDC exceeds --max-pay ${maxPay}`
|
|
1715
|
-
};
|
|
1716
|
-
}
|
|
1717
|
-
});
|
|
1718
|
-
const httpClient = new x402HTTPClient(client).onPaymentRequired(
|
|
1719
|
-
createSIWxClientHook(this.account)
|
|
1720
|
-
);
|
|
1721
|
-
const wrappedFetch = wrapFetchWithPayment(fetch, httpClient);
|
|
1722
|
-
const response = await wrappedFetch(url, {
|
|
1723
|
-
method: request.method,
|
|
1724
|
-
headers: request.headers,
|
|
1725
|
-
body: request.body
|
|
1726
|
-
});
|
|
1727
|
-
let txHash = null;
|
|
1728
|
-
const paymentResponseHeader = response.headers.get("payment-response") ?? response.headers.get("x-payment-response");
|
|
1729
|
-
if (paymentResponseHeader) {
|
|
1730
|
-
try {
|
|
1731
|
-
const settlement = decodePaymentResponseHeader(paymentResponseHeader);
|
|
1732
|
-
txHash = settlement.transaction ?? null;
|
|
1733
|
-
} catch {
|
|
1734
|
-
}
|
|
1735
|
-
}
|
|
1736
|
-
return {
|
|
1737
|
-
response,
|
|
1738
|
-
protocol: "x402",
|
|
1739
|
-
chain: "base",
|
|
1740
|
-
txHash,
|
|
1741
|
-
amount: capturedAmount,
|
|
1742
|
-
asset: "USDC"
|
|
1743
|
-
};
|
|
1744
|
-
};
|
|
1745
|
-
payMpp = async (url, request, _raw, maxPay, onProgress) => {
|
|
1746
|
-
if (!this.account) throw new Error("No wallet configured");
|
|
1747
|
-
let capturedAmount = "0";
|
|
1748
|
-
const mppx = Mppx.create({
|
|
1749
|
-
polyfill: false,
|
|
1750
|
-
methods: [
|
|
1751
|
-
tempo({
|
|
1752
|
-
account: this.account,
|
|
1753
|
-
maxDeposit: maxPay ?? DEFAULT_MAX_DEPOSIT
|
|
1754
|
-
})
|
|
1755
|
-
],
|
|
1756
|
-
onChallenge: async (challenge) => {
|
|
1757
|
-
const challengeRequest = challenge.request;
|
|
1758
|
-
const intent = challenge.intent;
|
|
1759
|
-
let requiredRaw;
|
|
1760
|
-
if (intent === "session") {
|
|
1761
|
-
const suggestedDeposit = challengeRequest.suggestedDeposit;
|
|
1762
|
-
if (suggestedDeposit) {
|
|
1763
|
-
requiredRaw = BigInt(suggestedDeposit);
|
|
1764
|
-
} else {
|
|
1765
|
-
const depositStr = maxPay ?? DEFAULT_MAX_DEPOSIT;
|
|
1766
|
-
requiredRaw = BigInt(
|
|
1767
|
-
Math.floor(Number.parseFloat(depositStr) * 1e6)
|
|
1768
|
-
);
|
|
1769
|
-
}
|
|
1770
|
-
} else {
|
|
1771
|
-
requiredRaw = BigInt(challengeRequest.amount);
|
|
1772
|
-
}
|
|
1773
|
-
capturedAmount = formatUnits(requiredRaw, 6);
|
|
1774
|
-
if (maxPay && Number.parseFloat(capturedAmount) > Number.parseFloat(maxPay)) {
|
|
1775
|
-
throw new Error(
|
|
1776
|
-
`Payment of ${capturedAmount} USDC exceeds --max-pay ${maxPay}`
|
|
1777
|
-
);
|
|
1778
|
-
}
|
|
1779
|
-
const methodDetails = challengeRequest.methodDetails;
|
|
1780
|
-
const challengeChainId = challengeRequest.chainId ?? methodDetails?.chainId;
|
|
1781
|
-
const isTestnet = challengeChainId === TEMPO_TESTNET_CHAIN_ID;
|
|
1782
|
-
const balanceChain = isTestnet ? "tempo-testnet" : "tempo";
|
|
1783
|
-
onProgress?.(`Checking Tempo balance...`);
|
|
1784
|
-
const tempoBalance = await this.getBalanceRaw(balanceChain);
|
|
1785
|
-
if (tempoBalance < requiredRaw) {
|
|
1786
|
-
if (isTestnet) {
|
|
1787
|
-
throw new Error(
|
|
1788
|
-
`Insufficient pathUSD on Tempo testnet: have ${formatUnits(tempoBalance, 6)}, need ${capturedAmount}. Fund your wallet with the Tempo testnet faucet: https://docs.tempo.xyz/quickstart/faucet`
|
|
1789
|
-
);
|
|
1790
|
-
}
|
|
1791
|
-
await this.bridgeToTempo(requiredRaw, onProgress);
|
|
1792
|
-
}
|
|
1793
|
-
return void 0;
|
|
1794
|
-
}
|
|
1795
|
-
});
|
|
1796
|
-
const response = await mppx.fetch(url, {
|
|
1797
|
-
method: request.method,
|
|
1798
|
-
headers: request.headers,
|
|
1799
|
-
body: request.body
|
|
1800
|
-
});
|
|
1801
|
-
let txHash = null;
|
|
1802
|
-
try {
|
|
1803
|
-
const receipt = Receipt.fromResponse(response);
|
|
1804
|
-
txHash = receipt.reference ?? null;
|
|
1805
|
-
} catch {
|
|
1806
|
-
}
|
|
1807
|
-
return {
|
|
1808
|
-
response,
|
|
1809
|
-
protocol: "mpp",
|
|
1810
|
-
chain: "tempo",
|
|
1811
|
-
txHash,
|
|
1812
|
-
amount: capturedAmount,
|
|
1813
|
-
asset: "USDC"
|
|
1814
|
-
};
|
|
1815
|
-
};
|
|
1816
|
-
resolveChainConfig = (chain) => {
|
|
1817
|
-
switch (chain) {
|
|
1818
|
-
case "base":
|
|
1819
|
-
return { viemChain: base, token: USDC_BASE };
|
|
1820
|
-
case "base-sepolia":
|
|
1821
|
-
return { viemChain: baseSepolia, token: USDC_BASE_SEPOLIA };
|
|
1822
|
-
case "tempo":
|
|
1823
|
-
return { viemChain: tempoChain, token: USDC_TEMPO };
|
|
1824
|
-
case "tempo-testnet":
|
|
1825
|
-
return { viemChain: tempoTestnetChain, token: PATHUSD_TEMPO };
|
|
1826
|
-
}
|
|
1827
|
-
};
|
|
1828
|
-
getBalanceRaw = async (chain) => {
|
|
1829
|
-
if (!this.account) return 0n;
|
|
1830
|
-
const { viemChain, token } = this.resolveChainConfig(chain);
|
|
1831
|
-
const client = createPublicClient({
|
|
1832
|
-
chain: viemChain,
|
|
1833
|
-
transport: http()
|
|
1834
|
-
});
|
|
1835
|
-
const balance = await client.readContract({
|
|
1836
|
-
address: token,
|
|
1837
|
-
abi: ERC20_BALANCE_ABI,
|
|
1838
|
-
functionName: "balanceOf",
|
|
1839
|
-
args: [this.account.address]
|
|
1840
|
-
});
|
|
1841
|
-
return balance;
|
|
1842
|
-
};
|
|
1843
|
-
getBalance = async (chain) => {
|
|
1844
|
-
const raw = await this.getBalanceRaw(chain);
|
|
1845
|
-
return { amount: formatUnits(raw, 6), asset: "USDC" };
|
|
1846
|
-
};
|
|
1847
|
-
};
|
|
1848
|
-
|
|
1849
2443
|
// src/services/state-service.ts
|
|
1850
|
-
import { existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as
|
|
2444
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
|
|
1851
2445
|
import { join as join4 } from "path";
|
|
1852
2446
|
var StateService = class {
|
|
1853
2447
|
constructor(zeroDir) {
|
|
@@ -1862,7 +2456,7 @@ var StateService = class {
|
|
|
1862
2456
|
loadLastSearch = () => {
|
|
1863
2457
|
try {
|
|
1864
2458
|
if (!existsSync5(this.lastSearchPath)) return null;
|
|
1865
|
-
const raw =
|
|
2459
|
+
const raw = readFileSync7(this.lastSearchPath, "utf8");
|
|
1866
2460
|
return JSON.parse(raw);
|
|
1867
2461
|
} catch {
|
|
1868
2462
|
return null;
|
|
@@ -1906,7 +2500,7 @@ var getServices = (env) => {
|
|
|
1906
2500
|
if (!privateKey) {
|
|
1907
2501
|
try {
|
|
1908
2502
|
if (existsSync6(configPath)) {
|
|
1909
|
-
const config = JSON.parse(
|
|
2503
|
+
const config = JSON.parse(readFileSync8(configPath, "utf8"));
|
|
1910
2504
|
if (typeof config.privateKey === "string") {
|
|
1911
2505
|
privateKey = config.privateKey;
|
|
1912
2506
|
}
|
|
@@ -1918,7 +2512,7 @@ var getServices = (env) => {
|
|
|
1918
2512
|
let lowBalanceWarning = 1;
|
|
1919
2513
|
try {
|
|
1920
2514
|
if (existsSync6(configPath)) {
|
|
1921
|
-
const config = JSON.parse(
|
|
2515
|
+
const config = JSON.parse(readFileSync8(configPath, "utf8"));
|
|
1922
2516
|
if (typeof config.lowBalanceWarning === "number") {
|
|
1923
2517
|
lowBalanceWarning = config.lowBalanceWarning;
|
|
1924
2518
|
}
|