@absol-labs/agent 0.12.0-rc.2 → 0.12.0-rc.4
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/cli/commands/claim.d.ts +15 -0
- package/dist/cli/commands/claim.d.ts.map +1 -1
- package/dist/cli/commands/claim.js +65 -23
- package/dist/cli/commands/claim.js.map +1 -1
- package/dist/cli/commands/pay.d.ts +10 -0
- package/dist/cli/commands/pay.d.ts.map +1 -1
- package/dist/cli/commands/pay.js +246 -50
- package/dist/cli/commands/pay.js.map +1 -1
- package/dist/cli/commands/refund.d.ts +23 -12
- package/dist/cli/commands/refund.d.ts.map +1 -1
- package/dist/cli/commands/refund.js +104 -131
- package/dist/cli/commands/refund.js.map +1 -1
- package/dist/cli/deps.d.ts +6 -0
- package/dist/cli/deps.d.ts.map +1 -1
- package/dist/cli/deps.js +3 -0
- package/dist/cli/deps.js.map +1 -1
- package/dist/cli/errors.d.ts +8 -1
- package/dist/cli/errors.d.ts.map +1 -1
- package/dist/cli/errors.js.map +1 -1
- package/dist/cli/status.d.ts +6 -0
- package/dist/cli/status.d.ts.map +1 -1
- package/dist/cli/status.js +25 -6
- package/dist/cli/status.js.map +1 -1
- package/dist/gateway/x402-opener-mode.d.ts.map +1 -1
- package/dist/gateway/x402-opener-mode.js +104 -7
- package/dist/gateway/x402-opener-mode.js.map +1 -1
- package/dist/mcp/server.d.ts +10 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +21 -1
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/x402-tools.d.ts +7 -1
- package/dist/mcp/x402-tools.d.ts.map +1 -1
- package/dist/mcp/x402-tools.js +159 -6
- package/dist/mcp/x402-tools.js.map +1 -1
- package/dist/x402-buyer/index.d.ts +1 -0
- package/dist/x402-buyer/index.d.ts.map +1 -1
- package/dist/x402-buyer/index.js +1 -0
- package/dist/x402-buyer/index.js.map +1 -1
- package/dist/x402-buyer/pay.d.ts +38 -2
- package/dist/x402-buyer/pay.d.ts.map +1 -1
- package/dist/x402-buyer/pay.js +52 -2
- package/dist/x402-buyer/pay.js.map +1 -1
- package/dist/x402-buyer/refund-actions.d.ts +168 -0
- package/dist/x402-buyer/refund-actions.d.ts.map +1 -0
- package/dist/x402-buyer/refund-actions.js +351 -0
- package/dist/x402-buyer/refund-actions.js.map +1 -0
- package/dist/x402-opener/block-floors.d.ts +35 -0
- package/dist/x402-opener/block-floors.d.ts.map +1 -0
- package/dist/x402-opener/block-floors.js +72 -0
- package/dist/x402-opener/block-floors.js.map +1 -0
- package/dist/x402-opener/facilitator-server.d.ts.map +1 -1
- package/dist/x402-opener/facilitator-server.js +7 -0
- package/dist/x402-opener/facilitator-server.js.map +1 -1
- package/dist/x402-opener/index.d.ts +1 -0
- package/dist/x402-opener/index.d.ts.map +1 -1
- package/dist/x402-opener/index.js +1 -0
- package/dist/x402-opener/index.js.map +1 -1
- package/dist/x402-opener/refund-relay.d.ts +62 -1
- package/dist/x402-opener/refund-relay.d.ts.map +1 -1
- package/dist/x402-opener/refund-relay.js +387 -208
- package/dist/x402-opener/refund-relay.js.map +1 -1
- package/dist/x402-opener/scheme.d.ts +8 -0
- package/dist/x402-opener/scheme.d.ts.map +1 -1
- package/dist/x402-opener/scheme.js +14 -0
- package/dist/x402-opener/scheme.js.map +1 -1
- package/docs/cli.md +62 -9
- package/docs/mcp-reference.md +23 -0
- package/docs/x402.md +126 -29
- package/package.json +2 -2
- package/src/cli/commands/claim.ts +72 -30
- package/src/cli/commands/pay.ts +299 -51
- package/src/cli/commands/refund.ts +133 -143
- package/src/cli/deps.ts +9 -0
- package/src/cli/errors.ts +8 -1
- package/src/cli/status.ts +28 -7
- package/src/gateway/x402-opener-mode.ts +131 -6
- package/src/mcp/server.ts +30 -1
- package/src/mcp/x402-tools.ts +222 -7
- package/src/x402-buyer/index.ts +1 -0
- package/src/x402-buyer/pay.ts +99 -2
- package/src/x402-buyer/refund-actions.ts +486 -0
- package/src/x402-opener/block-floors.ts +78 -0
- package/src/x402-opener/facilitator-server.ts +7 -0
- package/src/x402-opener/index.ts +1 -0
- package/src/x402-opener/refund-relay.ts +486 -217
- package/src/x402-opener/scheme.ts +38 -0
package/src/cli/commands/pay.ts
CHANGED
|
@@ -32,6 +32,17 @@ import {
|
|
|
32
32
|
} from "../wallet.js";
|
|
33
33
|
import { hasHeader, parseHeaders } from "./invoke.js";
|
|
34
34
|
import type { X402PostSignatureError } from "../../x402-buyer/pay.js";
|
|
35
|
+
import {
|
|
36
|
+
closeAfterPayment,
|
|
37
|
+
type CloseAfterOutcome,
|
|
38
|
+
} from "../../x402-buyer/refund-actions.js";
|
|
39
|
+
import {
|
|
40
|
+
authorizeCloseSigner,
|
|
41
|
+
DEFAULT_ATTEMPTS,
|
|
42
|
+
DEFAULT_MAX_WAIT_SECONDS,
|
|
43
|
+
explainRefundFailure,
|
|
44
|
+
reclaimActivity,
|
|
45
|
+
} from "./refund.js";
|
|
35
46
|
import type { CommandContext, CommandSpec } from "../context.js";
|
|
36
47
|
|
|
37
48
|
/** Default stream window a request-sized mandate allows (one day). */
|
|
@@ -44,7 +55,7 @@ export const payCommand: CommandSpec = {
|
|
|
44
55
|
summary:
|
|
45
56
|
"Pay an x402 endpoint into a recoverable escrow stream, under a spend mandate",
|
|
46
57
|
usage:
|
|
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]',
|
|
58
|
+
'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>] [--close-after] [--units atomic]',
|
|
48
59
|
flags: [
|
|
49
60
|
"data",
|
|
50
61
|
"json-body",
|
|
@@ -56,6 +67,7 @@ export const payCommand: CommandSpec = {
|
|
|
56
67
|
"max-duration",
|
|
57
68
|
"min-duration",
|
|
58
69
|
"operator",
|
|
70
|
+
"close-after",
|
|
59
71
|
"units",
|
|
60
72
|
"allow-legacy-factory",
|
|
61
73
|
],
|
|
@@ -78,6 +90,8 @@ export const payCommand: CommandSpec = {
|
|
|
78
90
|
"--max-upfront <usdc> (cap on the NON-REFUNDABLE per-call part of an upfront-opener quote; default 0.10 USDC, 0 refuses any)",
|
|
79
91
|
data: "--data <body>",
|
|
80
92
|
"json-body": "--json <body> (alias: --json-body <body>)",
|
|
93
|
+
"close-after":
|
|
94
|
+
"--close-after (after a successful paid response, this client closes the stream for you and reclaims the unearned part: it signs the payer's Close, the seller's refund relay sends it and pays the gas, then it waits for the covering checkpoint and asks the relay to reclaim to the payer. Your request, sent by your client; not an automatic refund. Exit 5 if it does not finish, with the command that finishes it)",
|
|
81
95
|
},
|
|
82
96
|
async run(context: CommandContext): Promise<void> {
|
|
83
97
|
const { args, config, env, out, deps } = context;
|
|
@@ -207,7 +221,16 @@ export const payCommand: CommandSpec = {
|
|
|
207
221
|
const opener = result.metrikStreamOpener;
|
|
208
222
|
const quote = result.quote;
|
|
209
223
|
const settlementTx = settlementTransaction(result.paymentResponse);
|
|
210
|
-
|
|
224
|
+
const closeAfter = booleanFlag(args, "close-after");
|
|
225
|
+
// A stream on another (e.g. legacy) registered escrow is reached by
|
|
226
|
+
// naming that escrow; the CLI defaults to the network's current one.
|
|
227
|
+
const escrowFlag =
|
|
228
|
+
opener?.escrow !== null &&
|
|
229
|
+
opener?.escrow !== undefined &&
|
|
230
|
+
opener.escrow.toLowerCase() !== config.escrow.toLowerCase()
|
|
231
|
+
? ` --escrow ${opener.escrow}`
|
|
232
|
+
: "";
|
|
233
|
+
const payResult = {
|
|
211
234
|
paid: result.paid,
|
|
212
235
|
// The facilitator's settlement transaction, from the PAYMENT-RESPONSE
|
|
213
236
|
// header the seller relayed: the facilitator's relayer broadcast it, not
|
|
@@ -240,19 +263,41 @@ export const payCommand: CommandSpec = {
|
|
|
240
263
|
minEarnSeconds: quote?.minEarnSeconds ?? null,
|
|
241
264
|
mandateSource: envCaps === null ? "request" : "env",
|
|
242
265
|
refundIsAutomatic: false,
|
|
243
|
-
}
|
|
266
|
+
};
|
|
244
267
|
|
|
245
268
|
if (!result.paid) {
|
|
269
|
+
// Nothing settled (the endpoint answered without a payment, or the paid
|
|
270
|
+
// request failed and was never settled): there is nothing to close.
|
|
271
|
+
out.result({
|
|
272
|
+
...payResult,
|
|
273
|
+
...(closeAfter ? { closeAfter: { outcome: "not-paid" } } : {}),
|
|
274
|
+
});
|
|
246
275
|
out.print(
|
|
247
276
|
`The endpoint answered ${result.response.status} without asking for payment; nothing was signed or paid.`,
|
|
248
277
|
);
|
|
249
278
|
out.print(truncate(bodyText));
|
|
279
|
+
if (closeAfter) {
|
|
280
|
+
out.print(
|
|
281
|
+
"--close-after: nothing was paid, so there is nothing to close.",
|
|
282
|
+
);
|
|
283
|
+
}
|
|
250
284
|
return;
|
|
251
285
|
}
|
|
252
286
|
out.print(`Paid. HTTP ${result.response.status}`);
|
|
253
287
|
out.print(truncate(bodyText));
|
|
254
288
|
out.print("");
|
|
255
289
|
if (opener !== null && opener.streamId === null) {
|
|
290
|
+
out.result({
|
|
291
|
+
...payResult,
|
|
292
|
+
...(closeAfter
|
|
293
|
+
? {
|
|
294
|
+
closeAfter: {
|
|
295
|
+
outcome: "nothing-to-close",
|
|
296
|
+
refundIsAutomatic: false,
|
|
297
|
+
},
|
|
298
|
+
}
|
|
299
|
+
: {}),
|
|
300
|
+
});
|
|
256
301
|
// Upfront-only: no stream exists, so there is no refund or reclaim to offer.
|
|
257
302
|
out.table([
|
|
258
303
|
["opener", opener.opener],
|
|
@@ -271,54 +316,62 @@ export const payCommand: CommandSpec = {
|
|
|
271
316
|
out.print(
|
|
272
317
|
"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
318
|
);
|
|
319
|
+
if (closeAfter) {
|
|
320
|
+
out.print(
|
|
321
|
+
"--close-after: nothing to close — no stream was opened. Nothing was signed or sent.",
|
|
322
|
+
);
|
|
323
|
+
}
|
|
274
324
|
return;
|
|
275
325
|
}
|
|
276
|
-
if (opener
|
|
277
|
-
out.
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
326
|
+
if (opener === null || opener.streamId === null) {
|
|
327
|
+
out.result(payResult);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
const streamId = opener.streamId;
|
|
331
|
+
out.table([
|
|
332
|
+
["streamId", streamId],
|
|
333
|
+
["opener", opener.opener],
|
|
334
|
+
["payer", account.address],
|
|
335
|
+
...splitRows(quote, opener.upfrontFee),
|
|
336
|
+
...(opener.deposit === null
|
|
337
|
+
? []
|
|
338
|
+
: ([["deposit", formatUsdc(opener.deposit)]] as const)),
|
|
339
|
+
...(opener.ratePerSecond === null
|
|
340
|
+
? []
|
|
341
|
+
: ([
|
|
342
|
+
["rate", `${formatUsdc(opener.ratePerSecond)} / verified second`],
|
|
343
|
+
] as const)),
|
|
344
|
+
...(quote?.minEarnSeconds === null || quote?.minEarnSeconds === undefined
|
|
345
|
+
? []
|
|
346
|
+
: ([
|
|
347
|
+
[
|
|
348
|
+
"fully earned after",
|
|
349
|
+
`${formatDuration(Number(quote.minEarnSeconds))} of verified delivery`,
|
|
350
|
+
],
|
|
351
|
+
] as const)),
|
|
352
|
+
...(opener.expiresAt === null
|
|
353
|
+
? []
|
|
354
|
+
: ([
|
|
355
|
+
["expires", new Date(opener.expiresAt * 1000).toISOString()],
|
|
356
|
+
] as const)),
|
|
357
|
+
["refund url", opener.refundUrl ?? "(the seller advertised no relay)"],
|
|
358
|
+
...(settlementTx === null
|
|
359
|
+
? []
|
|
360
|
+
: ([
|
|
361
|
+
[
|
|
362
|
+
"settlement tx",
|
|
363
|
+
`${settlementTx} (reported by the seller's facilitator in PAYMENT-RESPONSE; its relayer broadcast it)`,
|
|
364
|
+
],
|
|
365
|
+
] as const)),
|
|
366
|
+
]);
|
|
367
|
+
out.print("");
|
|
368
|
+
const refundCommand = (flags: string): string =>
|
|
369
|
+
opener.refundUrl === null
|
|
370
|
+
? `metrik reclaim ${streamId}${escrowFlag}`
|
|
371
|
+
: `metrik refund ${streamId} --refund-url ${opener.refundUrl}${escrowFlag} ${flags}`;
|
|
372
|
+
|
|
373
|
+
if (!closeAfter) {
|
|
374
|
+
out.result(payResult);
|
|
322
375
|
out.print(
|
|
323
376
|
quote?.openerKind === "upfront" &&
|
|
324
377
|
quote.upfrontAmount !== null &&
|
|
@@ -328,13 +381,208 @@ export const payCommand: CommandSpec = {
|
|
|
328
381
|
);
|
|
329
382
|
out.print(
|
|
330
383
|
opener.refundUrl === null
|
|
331
|
-
? `
|
|
332
|
-
: `
|
|
384
|
+
? ` ${refundCommand("")} (anyone may send it; the refund always goes to the payer)`
|
|
385
|
+
: ` ${refundCommand("--close --reclaim")}`,
|
|
333
386
|
);
|
|
387
|
+
return;
|
|
334
388
|
}
|
|
389
|
+
|
|
390
|
+
// --close-after: the buyer asked for it, so THIS client now sends the
|
|
391
|
+
// buyer's own close and reclaim. Not an automatic refund.
|
|
392
|
+
out.note(
|
|
393
|
+
"--close-after: closing the stream now, as you asked. This client signs the payer's Close and the seller's refund relay sends it (the relay pays the gas); then it waits for the checkpoint covering the close and asks the relay to reclaim the unearned part to the payer.",
|
|
394
|
+
);
|
|
395
|
+
const outcome = await closeAfterPayment({
|
|
396
|
+
payment: opener,
|
|
397
|
+
payer: account.address,
|
|
398
|
+
openRefunds: async () =>
|
|
399
|
+
await x402.refundClient(config, {
|
|
400
|
+
refundUrl: opener.refundUrl as string,
|
|
401
|
+
streamId,
|
|
402
|
+
opener: opener.opener,
|
|
403
|
+
maxAttempts: DEFAULT_ATTEMPTS,
|
|
404
|
+
maxDelayMs: DEFAULT_MAX_WAIT_SECONDS * 1_000,
|
|
405
|
+
...(typeof opener.settledAt === "number"
|
|
406
|
+
? { settledAt: opener.settledAt }
|
|
407
|
+
: {}),
|
|
408
|
+
}),
|
|
409
|
+
closeSigner: async () =>
|
|
410
|
+
await authorizeCloseSigner(account, config.chainId),
|
|
411
|
+
sleep: deps.sleep,
|
|
412
|
+
note: out.note,
|
|
413
|
+
...(context.progress === undefined
|
|
414
|
+
? {}
|
|
415
|
+
: {
|
|
416
|
+
setActivity: (activity: string | null) =>
|
|
417
|
+
context.progress?.setActivity(activity),
|
|
418
|
+
}),
|
|
419
|
+
reclaimActivity: reclaimActivity(
|
|
420
|
+
DEFAULT_ATTEMPTS,
|
|
421
|
+
DEFAULT_MAX_WAIT_SECONDS,
|
|
422
|
+
),
|
|
423
|
+
});
|
|
424
|
+
const finishCommand =
|
|
425
|
+
outcome.outcome === "incomplete"
|
|
426
|
+
? refundCommand(
|
|
427
|
+
outcome.incomplete?.closed === true
|
|
428
|
+
? "--reclaim"
|
|
429
|
+
: "--close --reclaim",
|
|
430
|
+
)
|
|
431
|
+
: null;
|
|
432
|
+
out.result({
|
|
433
|
+
...payResult,
|
|
434
|
+
closeAfter: describeCloseAfter(outcome, {
|
|
435
|
+
price: quote?.amount ?? null,
|
|
436
|
+
deposit: opener.deposit,
|
|
437
|
+
finishCommand,
|
|
438
|
+
}),
|
|
439
|
+
});
|
|
440
|
+
out.table(closeAfterRows(outcome, quote, opener.deposit));
|
|
441
|
+
out.print("");
|
|
442
|
+
if (outcome.outcome === "incomplete") {
|
|
443
|
+
const incomplete = outcome.incomplete;
|
|
444
|
+
const why =
|
|
445
|
+
incomplete?.error instanceof Error
|
|
446
|
+
? explainRefundFailure(incomplete.error)
|
|
447
|
+
: null;
|
|
448
|
+
throw new CliError(
|
|
449
|
+
`paid (HTTP ${result.response.status}), but the close-after did not finish: ${incomplete?.stage === "no-relay" ? "" : `the ${incomplete?.stage ?? "close"} did not finish — `}${incomplete?.reason ?? "unknown"}`,
|
|
450
|
+
{
|
|
451
|
+
exitCode: 5,
|
|
452
|
+
hint: `the payment and the response are fine, and nothing is lost: ${incomplete?.closed === true ? "the stream is closed and " : ""}the unearned part stays in the escrow for the payer. Finish it with:\n ${finishCommand ?? refundCommand("--close --reclaim")}${
|
|
453
|
+
why instanceof CliError && why.hint !== undefined
|
|
454
|
+
? `\n (${why.hint})`
|
|
455
|
+
: ""
|
|
456
|
+
}`,
|
|
457
|
+
cause: incomplete?.error ?? undefined,
|
|
458
|
+
},
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
out.print(describeCloseAfterDone(outcome));
|
|
335
462
|
},
|
|
336
463
|
};
|
|
337
464
|
|
|
465
|
+
/**
|
|
466
|
+
* The `closeAfter` part of `metrik pay --close-after --json`: what this
|
|
467
|
+
* client's close + reclaim did, with the amounts from the verified receipt.
|
|
468
|
+
*/
|
|
469
|
+
export function describeCloseAfter(
|
|
470
|
+
outcome: CloseAfterOutcome,
|
|
471
|
+
context: {
|
|
472
|
+
readonly price: bigint | null;
|
|
473
|
+
readonly deposit: bigint | null;
|
|
474
|
+
readonly finishCommand: string | null;
|
|
475
|
+
},
|
|
476
|
+
): Record<string, unknown> {
|
|
477
|
+
const refunded = outcome.refundedUsdc;
|
|
478
|
+
return {
|
|
479
|
+
outcome: outcome.outcome,
|
|
480
|
+
streamId: outcome.streamId,
|
|
481
|
+
close:
|
|
482
|
+
outcome.close === null
|
|
483
|
+
? null
|
|
484
|
+
: {
|
|
485
|
+
transaction: outcome.close.transaction,
|
|
486
|
+
verified: outcome.close.verified,
|
|
487
|
+
},
|
|
488
|
+
closeSkipped: outcome.closeSkipped,
|
|
489
|
+
reclaim:
|
|
490
|
+
outcome.reclaim === null
|
|
491
|
+
? null
|
|
492
|
+
: {
|
|
493
|
+
transaction: outcome.reclaim.transaction,
|
|
494
|
+
amount: outcome.reclaim.amount,
|
|
495
|
+
verified: outcome.reclaim.verified,
|
|
496
|
+
},
|
|
497
|
+
reclaimSkipped: outcome.reclaimSkipped,
|
|
498
|
+
// Base units returned to the payer, from the receipt's ReclaimForwarded
|
|
499
|
+
// (null unless the receipt was verified).
|
|
500
|
+
refunded,
|
|
501
|
+
// What the payment finally cost: price - refunded (upfront + earned).
|
|
502
|
+
kept:
|
|
503
|
+
refunded === null || context.price === null
|
|
504
|
+
? null
|
|
505
|
+
: context.price - refunded,
|
|
506
|
+
// What the seller earned from the escrow: deposit - refunded.
|
|
507
|
+
earned:
|
|
508
|
+
refunded === null || context.deposit === null
|
|
509
|
+
? null
|
|
510
|
+
: context.deposit - refunded,
|
|
511
|
+
finishedReason: outcome.finished,
|
|
512
|
+
status: outcome.status?.status ?? null,
|
|
513
|
+
reclaimed: outcome.status?.reclaimed ?? null,
|
|
514
|
+
incomplete:
|
|
515
|
+
outcome.incomplete === null
|
|
516
|
+
? null
|
|
517
|
+
: {
|
|
518
|
+
stage: outcome.incomplete.stage,
|
|
519
|
+
closed: outcome.incomplete.closed,
|
|
520
|
+
reason: outcome.incomplete.reason,
|
|
521
|
+
},
|
|
522
|
+
finishCommand: context.finishCommand,
|
|
523
|
+
// The buyer's own client sent these, at the buyer's request.
|
|
524
|
+
sentBy: "buyer",
|
|
525
|
+
refundIsAutomatic: false,
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/** Human rows for a close-after run. */
|
|
530
|
+
function closeAfterRows(
|
|
531
|
+
outcome: CloseAfterOutcome,
|
|
532
|
+
quote: X402QuoteSummary | null,
|
|
533
|
+
deposit: bigint | null,
|
|
534
|
+
): (readonly [string, string])[] {
|
|
535
|
+
const rows: (readonly [string, string])[] = [];
|
|
536
|
+
if (outcome.close !== null) {
|
|
537
|
+
rows.push([
|
|
538
|
+
"close tx",
|
|
539
|
+
`${outcome.close.transaction}${outcome.close.verified ? " (verified)" : " (relay-reported)"}`,
|
|
540
|
+
]);
|
|
541
|
+
} else if (outcome.closeSkipped !== null) {
|
|
542
|
+
rows.push(["close", "skipped — already closed"]);
|
|
543
|
+
}
|
|
544
|
+
if (outcome.reclaim !== null) {
|
|
545
|
+
rows.push([
|
|
546
|
+
"reclaim tx",
|
|
547
|
+
`${outcome.reclaim.transaction}${outcome.reclaim.verified ? " (verified)" : " (relay-reported)"}`,
|
|
548
|
+
]);
|
|
549
|
+
const amount = outcome.refundedUsdc ?? outcome.reclaim.amount;
|
|
550
|
+
if (amount !== null) {
|
|
551
|
+
rows.push([
|
|
552
|
+
"refunded to payer",
|
|
553
|
+
`${formatUsdc(amount)}${outcome.refundedUsdc === null ? " (relay-reported; the receipt was NOT verified)" : " (from the verified receipt)"}`,
|
|
554
|
+
]);
|
|
555
|
+
if (deposit !== null) {
|
|
556
|
+
rows.push([
|
|
557
|
+
"seller earned",
|
|
558
|
+
`${formatUsdc(deposit - amount)} (verified seconds)`,
|
|
559
|
+
]);
|
|
560
|
+
}
|
|
561
|
+
if (quote !== null) {
|
|
562
|
+
rows.push([
|
|
563
|
+
"payment cost",
|
|
564
|
+
`${formatUsdc(quote.amount - amount)} of the ${formatUsdc(quote.amount)} paid`,
|
|
565
|
+
]);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
return rows;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function describeCloseAfterDone(outcome: CloseAfterOutcome): string {
|
|
573
|
+
if (outcome.reclaim !== null) {
|
|
574
|
+
const amount = outcome.refundedUsdc ?? outcome.reclaim.amount;
|
|
575
|
+
return `Closed and reclaimed at your request: ${amount === null ? "the unearned remainder" : formatUsdc(amount)} went back to the payer${outcome.refundedUsdc !== null ? " (receipt verified on-chain)" : ""}. Nothing further to do for this stream.`;
|
|
576
|
+
}
|
|
577
|
+
if (outcome.finished === "reclaimed") {
|
|
578
|
+
return "Already finished: the stream is closed and was already reclaimed to the payer. Nothing further to do.";
|
|
579
|
+
}
|
|
580
|
+
if (outcome.finished === "nothing-to-reclaim") {
|
|
581
|
+
return "Closed at your request. Nothing to reclaim: the verified delivery covers the whole deposit, so none of it is unearned.";
|
|
582
|
+
}
|
|
583
|
+
return "Closed at your request.";
|
|
584
|
+
}
|
|
585
|
+
|
|
338
586
|
/**
|
|
339
587
|
* One line stating what a quote commits the payer to, printed before signing:
|
|
340
588
|
* for an upfront-opener quote, the NON-REFUNDABLE per-call part and the
|