@absol-labs/agent 0.12.0-rc.1 → 0.12.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cli/commands/claim.d.ts +32 -0
- package/dist/cli/commands/claim.d.ts.map +1 -1
- package/dist/cli/commands/claim.js +115 -27
- package/dist/cli/commands/claim.js.map +1 -1
- package/dist/cli/commands/pay.d.ts +15 -0
- package/dist/cli/commands/pay.d.ts.map +1 -1
- package/dist/cli/commands/pay.js +131 -7
- package/dist/cli/commands/pay.js.map +1 -1
- package/dist/cli/commands/reclaim.d.ts.map +1 -1
- package/dist/cli/commands/reclaim.js +20 -4
- package/dist/cli/commands/reclaim.js.map +1 -1
- package/dist/cli/commands/refund.d.ts +19 -1
- package/dist/cli/commands/refund.d.ts.map +1 -1
- package/dist/cli/commands/refund.js +200 -27
- package/dist/cli/commands/refund.js.map +1 -1
- package/dist/cli/commands/status.d.ts.map +1 -1
- package/dist/cli/commands/status.js +11 -1
- package/dist/cli/commands/status.js.map +1 -1
- package/dist/cli/commands/watch.d.ts.map +1 -1
- package/dist/cli/commands/watch.js +2 -0
- package/dist/cli/commands/watch.js.map +1 -1
- package/dist/cli/context.d.ts +19 -0
- package/dist/cli/context.d.ts.map +1 -1
- package/dist/cli/deps.d.ts +20 -0
- package/dist/cli/deps.d.ts.map +1 -1
- package/dist/cli/deps.js +14 -0
- package/dist/cli/deps.js.map +1 -1
- package/dist/cli/progress.d.ts +13 -1
- package/dist/cli/progress.d.ts.map +1 -1
- package/dist/cli/progress.js +7 -1
- package/dist/cli/progress.js.map +1 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +26 -14
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/status.d.ts +103 -0
- package/dist/cli/status.d.ts.map +1 -1
- package/dist/cli/status.js +310 -41
- package/dist/cli/status.js.map +1 -1
- package/dist/cli/wallet.d.ts +1 -1
- package/dist/cli/wallet.d.ts.map +1 -1
- package/dist/cli/wallet.js +28 -4
- package/dist/cli/wallet.js.map +1 -1
- package/dist/gateway/http-server.d.ts +17 -0
- package/dist/gateway/http-server.d.ts.map +1 -1
- package/dist/gateway/http-server.js +76 -9
- package/dist/gateway/http-server.js.map +1 -1
- package/dist/gateway/x402-opener-mode.d.ts +39 -2
- package/dist/gateway/x402-opener-mode.d.ts.map +1 -1
- package/dist/gateway/x402-opener-mode.js +169 -36
- package/dist/gateway/x402-opener-mode.js.map +1 -1
- package/dist/mcp/server.d.ts +12 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +28 -9
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/x402-tools.d.ts +8 -3
- package/dist/mcp/x402-tools.d.ts.map +1 -1
- package/dist/mcp/x402-tools.js +126 -6
- package/dist/mcp/x402-tools.js.map +1 -1
- package/dist/sdk/client.d.ts +13 -0
- package/dist/sdk/client.d.ts.map +1 -1
- package/dist/sdk/client.js +15 -0
- package/dist/sdk/client.js.map +1 -1
- package/dist/x402-buyer/pay.d.ts +22 -0
- package/dist/x402-buyer/pay.d.ts.map +1 -1
- package/dist/x402-buyer/pay.js +33 -3
- package/dist/x402-buyer/pay.js.map +1 -1
- package/dist/x402-opener/facilitator-entry.js +4 -1
- package/dist/x402-opener/facilitator-entry.js.map +1 -1
- package/dist/x402-opener/facilitator-server.d.ts +29 -0
- package/dist/x402-opener/facilitator-server.d.ts.map +1 -1
- package/dist/x402-opener/facilitator-server.js +67 -1
- package/dist/x402-opener/facilitator-server.js.map +1 -1
- package/dist/x402-opener/refund-relay.d.ts +30 -4
- package/dist/x402-opener/refund-relay.d.ts.map +1 -1
- package/dist/x402-opener/refund-relay.js +123 -22
- package/dist/x402-opener/refund-relay.js.map +1 -1
- package/dist/x402-opener/scheme.d.ts +79 -0
- package/dist/x402-opener/scheme.d.ts.map +1 -1
- package/dist/x402-opener/scheme.js +326 -40
- package/dist/x402-opener/scheme.js.map +1 -1
- package/dist/x402-opener/terms.d.ts +61 -1
- package/dist/x402-opener/terms.d.ts.map +1 -1
- package/dist/x402-opener/terms.js +94 -2
- package/dist/x402-opener/terms.js.map +1 -1
- package/docs/cli.md +87 -9
- package/docs/mcp-reference.md +18 -2
- package/docs/quickstart.md +3 -3
- package/docs/x402.md +150 -10
- package/package.json +3 -3
- package/src/cli/commands/claim.ts +167 -32
- package/src/cli/commands/pay.ts +150 -8
- package/src/cli/commands/reclaim.ts +25 -5
- package/src/cli/commands/refund.ts +231 -33
- package/src/cli/commands/status.ts +11 -1
- package/src/cli/commands/watch.ts +4 -0
- package/src/cli/context.ts +16 -0
- package/src/cli/deps.ts +37 -0
- package/src/cli/progress.ts +21 -2
- package/src/cli/run.ts +26 -13
- package/src/cli/status.ts +440 -43
- package/src/cli/wallet.ts +44 -7
- package/src/gateway/http-server.ts +96 -8
- package/src/gateway/x402-opener-mode.ts +234 -37
- package/src/mcp/server.ts +43 -11
- package/src/mcp/x402-tools.ts +171 -7
- package/src/sdk/client.ts +25 -0
- package/src/x402-buyer/pay.ts +41 -3
- package/src/x402-opener/facilitator-entry.ts +4 -1
- package/src/x402-opener/facilitator-server.ts +101 -1
- package/src/x402-opener/refund-relay.ts +150 -20
- package/src/x402-opener/scheme.ts +459 -41
- package/src/x402-opener/terms.ts +155 -2
package/src/cli/commands/pay.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { X402PayerSigner } from "@absol-labs/sdk/x402";
|
|
1
|
+
import type { X402PayerSigner, X402QuoteSummary } from "@absol-labs/sdk/x402";
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
DEFAULT_SERVER_MANDATE_TTL_SECONDS,
|
|
@@ -24,7 +24,12 @@ import { assertTestnetRpc } from "../config.js";
|
|
|
24
24
|
import { rpcTransport, type CliX402 } from "../deps.js";
|
|
25
25
|
import { CliError, configError, usageError } from "../errors.js";
|
|
26
26
|
import { usdcFundingHint } from "../../network.js";
|
|
27
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
CDP_ENV_VARS,
|
|
29
|
+
PRIVATE_KEY_ENV,
|
|
30
|
+
isPrivyWalletSource,
|
|
31
|
+
resolveCliWallet,
|
|
32
|
+
} from "../wallet.js";
|
|
28
33
|
import { hasHeader, parseHeaders } from "./invoke.js";
|
|
29
34
|
import type { X402PostSignatureError } from "../../x402-buyer/pay.js";
|
|
30
35
|
import type { CommandContext, CommandSpec } from "../context.js";
|
|
@@ -39,7 +44,7 @@ export const payCommand: CommandSpec = {
|
|
|
39
44
|
summary:
|
|
40
45
|
"Pay an x402 endpoint into a recoverable escrow stream, under a spend mandate",
|
|
41
46
|
usage:
|
|
42
|
-
'metrik pay <url> [--json \'{"text":"hi"}\' | --data <body>] [--method POST] [--header "K: V"] --max-amount <usdc> [--max-rate <usdc/s>] [--max-duration <seconds>] [--min-duration <seconds>] [--operator <address>] [--units atomic]',
|
|
47
|
+
'metrik pay <url> [--json \'{"text":"hi"}\' | --data <body>] [--method POST] [--header "K: V"] --max-amount <usdc> [--max-rate <usdc/s>] [--max-upfront <usdc>] [--max-duration <seconds>] [--min-duration <seconds>] [--operator <address>] [--units atomic]',
|
|
43
48
|
flags: [
|
|
44
49
|
"data",
|
|
45
50
|
"json-body",
|
|
@@ -47,6 +52,7 @@ export const payCommand: CommandSpec = {
|
|
|
47
52
|
"header",
|
|
48
53
|
"max-amount",
|
|
49
54
|
"max-rate",
|
|
55
|
+
"max-upfront",
|
|
50
56
|
"max-duration",
|
|
51
57
|
"min-duration",
|
|
52
58
|
"operator",
|
|
@@ -57,6 +63,22 @@ export const payCommand: CommandSpec = {
|
|
|
57
63
|
// settles into the escrow). The PAYER broadcasts nothing and needs no gas,
|
|
58
64
|
// but it is a fund-moving command and is gated like one.
|
|
59
65
|
signs: true,
|
|
66
|
+
// ...so the generic "Broadcasts a transaction" help would be false, and so
|
|
67
|
+
// would listing Privy: the command refuses a Privy wallet (its broker does
|
|
68
|
+
// not sign EIP-3009). Say what actually happens.
|
|
69
|
+
walletHelp: {
|
|
70
|
+
tag: "[signs a USDC payment]",
|
|
71
|
+
text:
|
|
72
|
+
"Broadcasts nothing and needs no gas: the payer signs an EIP-3009 USDC authorization (TransferWithAuthorization) " +
|
|
73
|
+
"that the seller's facilitator settles into an escrow stream. Requires a wallet holding USDC in the environment: " +
|
|
74
|
+
`${PRIVATE_KEY_ENV} or ${CDP_ENV_VARS.join(" + ")}. A Privy wallet cannot pay x402: its broker does not sign EIP-3009.`,
|
|
75
|
+
},
|
|
76
|
+
flagHelp: {
|
|
77
|
+
"max-upfront":
|
|
78
|
+
"--max-upfront <usdc> (cap on the NON-REFUNDABLE per-call part of an upfront-opener quote; default 0.10 USDC, 0 refuses any)",
|
|
79
|
+
data: "--data <body>",
|
|
80
|
+
"json-body": "--json <body> (alias: --json-body <body>)",
|
|
81
|
+
},
|
|
60
82
|
async run(context: CommandContext): Promise<void> {
|
|
61
83
|
const { args, config, env, out, deps } = context;
|
|
62
84
|
const x402 = requireX402(deps.x402);
|
|
@@ -76,6 +98,11 @@ export const payCommand: CommandSpec = {
|
|
|
76
98
|
maxRateRaw === undefined
|
|
77
99
|
? undefined
|
|
78
100
|
: parseUsdcAmount(maxRateRaw, units, "max-rate");
|
|
101
|
+
const maxUpfrontRaw = stringFlag(args, "max-upfront");
|
|
102
|
+
const maxUpfront =
|
|
103
|
+
maxUpfrontRaw === undefined
|
|
104
|
+
? undefined
|
|
105
|
+
: parseUsdcAmount(maxUpfrontRaw, units, "max-upfront");
|
|
79
106
|
const maxDurationRaw = stringFlag(args, "max-duration");
|
|
80
107
|
const maxDuration =
|
|
81
108
|
maxDurationRaw === undefined
|
|
@@ -115,6 +142,7 @@ export const payCommand: CommandSpec = {
|
|
|
115
142
|
});
|
|
116
143
|
|
|
117
144
|
const wallet = await resolveCliWallet(env, {
|
|
145
|
+
purpose: "x402-payment",
|
|
118
146
|
note: out.note,
|
|
119
147
|
rpcUrl: config.rpcUrl,
|
|
120
148
|
});
|
|
@@ -158,6 +186,12 @@ export const payCommand: CommandSpec = {
|
|
|
158
186
|
...(maxAmount === undefined ? {} : { maxAmount }),
|
|
159
187
|
...(maxRate === undefined ? {} : { maxRatePerSecond: maxRate }),
|
|
160
188
|
...(minDuration === undefined ? {} : { minDurationSeconds: minDuration }),
|
|
189
|
+
...(maxUpfront === undefined ? {} : { maxUpfrontAmount: maxUpfront }),
|
|
190
|
+
// The split, stated BEFORE anything is signed (the mandate approved the
|
|
191
|
+
// quote; the SDK signs right after this).
|
|
192
|
+
onApproved: ({ quote }) => {
|
|
193
|
+
out.note(describeQuoteSplit(quote));
|
|
194
|
+
},
|
|
161
195
|
sdkOptions: {
|
|
162
196
|
transport: rpcTransport(config),
|
|
163
197
|
// A LEGACY deployment's factory is refused by the SDK unless the
|
|
@@ -172,8 +206,15 @@ export const payCommand: CommandSpec = {
|
|
|
172
206
|
const bodyText = await result.response.text();
|
|
173
207
|
const opener = result.metrikStreamOpener;
|
|
174
208
|
const quote = result.quote;
|
|
209
|
+
const settlementTx = settlementTransaction(result.paymentResponse);
|
|
175
210
|
out.result({
|
|
176
211
|
paid: result.paid,
|
|
212
|
+
// The facilitator's settlement transaction, from the PAYMENT-RESPONSE
|
|
213
|
+
// header the seller relayed: the facilitator's relayer broadcast it, not
|
|
214
|
+
// the payer. Reported as-is; null when the header carries none.
|
|
215
|
+
settlementTransaction: settlementTx,
|
|
216
|
+
settlementTransactionSource:
|
|
217
|
+
settlementTx === null ? null : "PAYMENT-RESPONSE",
|
|
177
218
|
httpStatus: result.response.status,
|
|
178
219
|
contentType: result.response.headers.get("content-type"),
|
|
179
220
|
body: bodyText,
|
|
@@ -182,6 +223,16 @@ export const payCommand: CommandSpec = {
|
|
|
182
223
|
refundUrl: opener?.refundUrl ?? null,
|
|
183
224
|
escrow: opener?.escrow ?? null,
|
|
184
225
|
deposit: opener?.deposit ?? null,
|
|
226
|
+
openerKind: quote?.openerKind ?? null,
|
|
227
|
+
price: quote?.amount ?? null,
|
|
228
|
+
// The NON-REFUNDABLE part (paid to the seller at settlement) and the
|
|
229
|
+
// metered, refundable part (the most any refund can return).
|
|
230
|
+
upfrontAmount: quote?.upfrontAmount ?? null,
|
|
231
|
+
refundableAmount: quote?.refundableAmount ?? null,
|
|
232
|
+
upfrontFee: opener?.upfrontFee ?? null,
|
|
233
|
+
upfrontOnly: opener?.upfrontOnly ?? quote?.upfrontOnly ?? null,
|
|
234
|
+
// True for an upfront-only payment: nothing was escrowed, nothing to refund.
|
|
235
|
+
final: opener?.upfrontOnly === true,
|
|
185
236
|
ratePerSecond: opener?.ratePerSecond ?? null,
|
|
186
237
|
expiresAt: opener?.expiresAt ?? null,
|
|
187
238
|
operator: quote?.operator ?? null,
|
|
@@ -201,11 +252,33 @@ export const payCommand: CommandSpec = {
|
|
|
201
252
|
out.print(`Paid. HTTP ${result.response.status}`);
|
|
202
253
|
out.print(truncate(bodyText));
|
|
203
254
|
out.print("");
|
|
204
|
-
if (opener !== null) {
|
|
255
|
+
if (opener !== null && opener.streamId === null) {
|
|
256
|
+
// Upfront-only: no stream exists, so there is no refund or reclaim to offer.
|
|
257
|
+
out.table([
|
|
258
|
+
["opener", opener.opener],
|
|
259
|
+
["payer", account.address],
|
|
260
|
+
...splitRows(quote, opener.upfrontFee),
|
|
261
|
+
...(settlementTx === null
|
|
262
|
+
? []
|
|
263
|
+
: ([
|
|
264
|
+
[
|
|
265
|
+
"settlement tx",
|
|
266
|
+
`${settlementTx} (reported by the seller's facilitator in PAYMENT-RESPONSE; its relayer broadcast it)`,
|
|
267
|
+
],
|
|
268
|
+
] as const)),
|
|
269
|
+
]);
|
|
270
|
+
out.print("");
|
|
271
|
+
out.print(
|
|
272
|
+
"This payment is FINAL: it was an upfront-only (minimum-charge) payment, the whole price was paid to the seller at settlement as a non-refundable per-call charge, and no escrow stream was opened. There is nothing to refund or reclaim.",
|
|
273
|
+
);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
if (opener !== null && opener.streamId !== null) {
|
|
205
277
|
out.table([
|
|
206
278
|
["streamId", opener.streamId],
|
|
207
279
|
["opener", opener.opener],
|
|
208
280
|
["payer", account.address],
|
|
281
|
+
...splitRows(quote, opener.upfrontFee),
|
|
209
282
|
...(opener.deposit === null
|
|
210
283
|
? []
|
|
211
284
|
: ([["deposit", formatUsdc(opener.deposit)]] as const)),
|
|
@@ -229,6 +302,14 @@ export const payCommand: CommandSpec = {
|
|
|
229
302
|
["expires", new Date(opener.expiresAt * 1000).toISOString()],
|
|
230
303
|
] as const)),
|
|
231
304
|
["refund url", opener.refundUrl ?? "(the seller advertised no relay)"],
|
|
305
|
+
...(settlementTx === null
|
|
306
|
+
? []
|
|
307
|
+
: ([
|
|
308
|
+
[
|
|
309
|
+
"settlement tx",
|
|
310
|
+
`${settlementTx} (reported by the seller's facilitator in PAYMENT-RESPONSE; its relayer broadcast it)`,
|
|
311
|
+
],
|
|
312
|
+
] as const)),
|
|
232
313
|
]);
|
|
233
314
|
out.print("");
|
|
234
315
|
// A stream on another (e.g. legacy) registered escrow is reached by
|
|
@@ -239,7 +320,11 @@ export const payCommand: CommandSpec = {
|
|
|
239
320
|
? ` --escrow ${opener.escrow}`
|
|
240
321
|
: "";
|
|
241
322
|
out.print(
|
|
242
|
-
|
|
323
|
+
quote?.openerKind === "upfront" &&
|
|
324
|
+
quote.upfrontAmount !== null &&
|
|
325
|
+
quote.upfrontAmount > 0n
|
|
326
|
+
? `Refunds are NOT automatic. The ${formatUsdc(quote.upfrontAmount)} upfront charge is final; of the ${formatUsdc(quote.refundableAmount ?? 0n)} escrowed, the seller earns only verified seconds, and whatever is unearned stays in the escrow until someone reclaims it for you:`
|
|
327
|
+
: "Refunds are NOT automatic. The seller earns only for verified seconds; whatever is unearned stays in the escrow until someone reclaims it for you:",
|
|
243
328
|
);
|
|
244
329
|
out.print(
|
|
245
330
|
opener.refundUrl === null
|
|
@@ -250,6 +335,54 @@ export const payCommand: CommandSpec = {
|
|
|
250
335
|
},
|
|
251
336
|
};
|
|
252
337
|
|
|
338
|
+
/**
|
|
339
|
+
* One line stating what a quote commits the payer to, printed before signing:
|
|
340
|
+
* for an upfront-opener quote, the NON-REFUNDABLE per-call part and the
|
|
341
|
+
* metered, refundable part; for a stream quote, that all of it is metered.
|
|
342
|
+
*/
|
|
343
|
+
export function describeQuoteSplit(quote: X402QuoteSummary): string {
|
|
344
|
+
if (quote.openerKind === "upfront" && quote.upfrontAmount !== null) {
|
|
345
|
+
if (quote.upfrontOnly === true) {
|
|
346
|
+
return `quote: ${formatUsdc(quote.amount)}, ALL of it a NON-REFUNDABLE per-call charge (upfront-only: no stream is opened, nothing is metered, nothing can be refunded). Signing it.`;
|
|
347
|
+
}
|
|
348
|
+
return `quote: ${formatUsdc(quote.amount)} = ${formatUsdc(quote.upfrontAmount)} NON-REFUNDABLE per-call charge (paid to the seller at settlement) + ${formatUsdc(quote.refundableAmount ?? 0n)} metered and refundable (escrowed; the seller earns it only for verified seconds). Signing it.`;
|
|
349
|
+
}
|
|
350
|
+
return `quote: ${formatUsdc(quote.amount)}, all of it escrowed, metered and refundable (the seller earns it only for verified seconds). Signing it.`;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** Table rows stating a paid quote's split (upfront quotes only). */
|
|
354
|
+
function splitRows(
|
|
355
|
+
quote: X402QuoteSummary | null,
|
|
356
|
+
upfrontFee: bigint | null,
|
|
357
|
+
): (readonly [string, string])[] {
|
|
358
|
+
if (quote === null || quote.openerKind !== "upfront") return [];
|
|
359
|
+
return [
|
|
360
|
+
["price", formatUsdc(quote.amount)],
|
|
361
|
+
[
|
|
362
|
+
"non-refundable",
|
|
363
|
+
`${formatUsdc(quote.upfrontAmount ?? 0n)} (per-call charge, paid to the seller at settlement${upfrontFee === null ? "" : `; protocol fee ${formatUsdc(upfrontFee)} as the seller reports it`})`,
|
|
364
|
+
],
|
|
365
|
+
[
|
|
366
|
+
"refundable",
|
|
367
|
+
`${formatUsdc(quote.refundableAmount ?? 0n)} (metered; the most any refund can return)`,
|
|
368
|
+
],
|
|
369
|
+
];
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* The settlement transaction hash from the facilitator's `PAYMENT-RESPONSE`,
|
|
374
|
+
* when it carries a well-formed one. Anything else (absent, empty, not a
|
|
375
|
+
* 32-byte hash) is `null`: a malformed value is never printed as a hash.
|
|
376
|
+
*/
|
|
377
|
+
export function settlementTransaction(
|
|
378
|
+
paymentResponse: { readonly transaction?: unknown } | null | undefined,
|
|
379
|
+
): `0x${string}` | null {
|
|
380
|
+
const value = paymentResponse?.transaction;
|
|
381
|
+
return typeof value === "string" && /^0x[0-9a-fA-F]{64}$/.test(value)
|
|
382
|
+
? (value as `0x${string}`)
|
|
383
|
+
: null;
|
|
384
|
+
}
|
|
385
|
+
|
|
253
386
|
const minBig = (a: bigint, b: bigint | undefined): bigint =>
|
|
254
387
|
b !== undefined && b < a ? b : a;
|
|
255
388
|
|
|
@@ -410,15 +543,24 @@ export function explainPayFailure(error: unknown): unknown {
|
|
|
410
543
|
case "X402PaymentRefusedError":
|
|
411
544
|
return new CliError(error.message, {
|
|
412
545
|
exitCode: 3,
|
|
413
|
-
hint: /
|
|
414
|
-
? "
|
|
415
|
-
:
|
|
546
|
+
hint: /maxUpfrontAmount/.test(error.message)
|
|
547
|
+
? "the quote's NON-REFUNDABLE upfront (per-call) charge is above your cap (--max-upfront, default 0.10 USDC). Raise --max-upfront only if you accept losing that much per call whatever is delivered. Nothing was signed."
|
|
548
|
+
: /exceeds maxAmount|exceeds maxRatePerSecond/.test(error.message)
|
|
549
|
+
? "that ceiling is your spend mandate's (--max-amount / --max-rate or METRIK_AGENT_MANDATE_*), handed to the SDK as a second gate. Nothing was signed."
|
|
550
|
+
: "the quote failed one of the SDK's protections (wrong network or token, unknown factory, payTo != factory.predict(terms), invalid upfront terms, not an opener payment, ...). Nothing was signed.",
|
|
416
551
|
cause: error,
|
|
417
552
|
});
|
|
418
553
|
case "X402PostSignatureError": {
|
|
419
554
|
// The signature was handed out: never say "nothing moved" unless the
|
|
420
555
|
// chain proves the authorization can never be used.
|
|
421
556
|
const post = error as X402PostSignatureError;
|
|
557
|
+
if (post.upfrontOnly && post.state === "used") {
|
|
558
|
+
return new CliError(error.message, {
|
|
559
|
+
exitCode: 4,
|
|
560
|
+
hint: "the money LEFT the payer as an upfront-only (minimum-charge) payment: the whole price was a non-refundable per-call charge and no stream was opened, so there is nothing to refund or reclaim. Keep this output as evidence if the response was not delivered (see docs/x402.md).",
|
|
561
|
+
cause: error,
|
|
562
|
+
});
|
|
563
|
+
}
|
|
422
564
|
const recover =
|
|
423
565
|
post.streamId === null
|
|
424
566
|
? ""
|
|
@@ -52,6 +52,8 @@ export const reclaimCommand: CommandSpec = {
|
|
|
52
52
|
signs: true,
|
|
53
53
|
async run(context: CommandContext): Promise<void> {
|
|
54
54
|
const { args, config, env, out, deps } = context;
|
|
55
|
+
const setActivity = (activity: string | null): void =>
|
|
56
|
+
context.progress?.setActivity(activity);
|
|
55
57
|
const [streamIdArg] = args.positionals;
|
|
56
58
|
if (streamIdArg === undefined) {
|
|
57
59
|
throw usageError("metrik reclaim requires a <streamId>");
|
|
@@ -136,7 +138,11 @@ export const reclaimCommand: CommandSpec = {
|
|
|
136
138
|
out.note(
|
|
137
139
|
`closed in ${closeResult.txHash} — waiting for it to land …`,
|
|
138
140
|
);
|
|
141
|
+
setActivity(
|
|
142
|
+
`waiting for the RPC endpoint to show the close in ${closeResult.txHash}`,
|
|
143
|
+
);
|
|
139
144
|
await awaitClosed(read, streamId, deps.sleep, closeResult.txHash);
|
|
145
|
+
setActivity(null);
|
|
140
146
|
// A stream closed before any checkpoint covers its stop time is the
|
|
141
147
|
// ORDINARY outcome of a short trial run, not an exotic one — and it
|
|
142
148
|
// is the state `--close-first` leaves behind by construction. Say
|
|
@@ -175,10 +181,15 @@ export const reclaimCommand: CommandSpec = {
|
|
|
175
181
|
),
|
|
176
182
|
explainFailure: async (error) =>
|
|
177
183
|
await explainReclaimFailure(error, read, streamId, deps.nowSeconds()),
|
|
178
|
-
onRetry: (attempt, reason) =>
|
|
184
|
+
onRetry: (attempt, reason) => {
|
|
179
185
|
out.note(
|
|
180
186
|
`reclaim blocked: no checkpoint covers the stream's stop time yet (attempt ${attempt}/${attempts}) — ${reason}`,
|
|
181
|
-
)
|
|
187
|
+
);
|
|
188
|
+
// The heartbeat must describe THIS wait, not guess at a slow RPC.
|
|
189
|
+
setActivity(
|
|
190
|
+
`waiting for the oracle's next checkpoint (attempt ${attempt}/${attempts} was blocked; retrying every ${interval}s)`,
|
|
191
|
+
);
|
|
192
|
+
},
|
|
182
193
|
// "Retry later" is the wrong advice when the oracle is gone: every
|
|
183
194
|
// attempt this command makes needs a proof FROM that oracle. So the
|
|
184
195
|
// give-up message names the path that needs no proof, and says whether
|
|
@@ -196,11 +207,13 @@ export const reclaimCommand: CommandSpec = {
|
|
|
196
207
|
// Read the settled figure back only once the node we are reading from has
|
|
197
208
|
// actually applied the reclaim. The public RPC is load-balanced, so an
|
|
198
209
|
// immediate read can land on a lagged node and report `settledCumulative: 0`
|
|
199
|
-
// for a stream that just settled a non-zero amount — printing
|
|
200
|
-
//
|
|
210
|
+
// for a stream that just settled a non-zero amount — printing an operator
|
|
211
|
+
// entitlement of 0 USDC when it was not zero. Poll for `reclaimed` (which the
|
|
201
212
|
// transaction we just landed sets) so the number beside it is from the same
|
|
202
213
|
// post-reclaim state. Bounded, and falls back to the last value read.
|
|
214
|
+
setActivity("waiting for the RPC endpoint to show the reclaim");
|
|
203
215
|
const stream = await readSettledStream(read, streamId, deps.sleep);
|
|
216
|
+
setActivity(null);
|
|
204
217
|
// The refund comes from the reclaim's own BuyerReclaimed event, never from
|
|
205
218
|
// a post-reclaim read: reclaim overwrites `deposit` with the operator's
|
|
206
219
|
// entitlement, so deposit − settled reads 0 after every reclaim.
|
|
@@ -239,7 +252,14 @@ export const reclaimCommand: CommandSpec = {
|
|
|
239
252
|
...(stream === null
|
|
240
253
|
? []
|
|
241
254
|
: ([
|
|
242
|
-
|
|
255
|
+
// No USDC moves to the operator at reclaim: reclaim fixes the
|
|
256
|
+
// operator's entitlement, the operator claims it later, and the
|
|
257
|
+
// protocol fee comes out at that claim. "Settled to operator"
|
|
258
|
+
// read as a payment that never happened.
|
|
259
|
+
[
|
|
260
|
+
"operator entitled",
|
|
261
|
+
`${formatUsdc(stream.settledCumulative)} (claims separately; fee taken at claim)`,
|
|
262
|
+
],
|
|
243
263
|
...(refunded === null
|
|
244
264
|
? []
|
|
245
265
|
: ([
|