@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
|
@@ -13,6 +13,12 @@ import type { StreamV2 } from "@absol-labs/sdk";
|
|
|
13
13
|
* learn a fact that was already available.
|
|
14
14
|
*/
|
|
15
15
|
export declare const claimCommand: CommandSpec;
|
|
16
|
+
/**
|
|
17
|
+
* A claim reverted because its proof no longer matches a root the escrow
|
|
18
|
+
* accepts (a checkpoint rotated, or a lagging node served a not-yet-visible
|
|
19
|
+
* root). Re-planning reads a fresh proof, so it is worth one more round.
|
|
20
|
+
*/
|
|
21
|
+
export declare function isStaleProofError(error: unknown): boolean;
|
|
16
22
|
/**
|
|
17
23
|
* Which claim, if any, `metrik claim` would send. Reads only, so it is
|
|
18
24
|
* testable (and checkable against a live stream) without a key.
|
|
@@ -42,4 +48,13 @@ export type ClaimPlan = {
|
|
|
42
48
|
readonly error: CliError;
|
|
43
49
|
};
|
|
44
50
|
export declare function planClaim(read: CliReadClient, streamId: `0x${string}`, stream: Pick<StreamV2, "claimedCumulative">, escrowGeneration: string): Promise<ClaimPlan>;
|
|
51
|
+
/**
|
|
52
|
+
* The buyer's recovery command for a stream. A reclaim needs the clock
|
|
53
|
+
* stopped (closed, or past expiresAt), so a still-running stream gets
|
|
54
|
+
* `--close-first`; the plain form only once it has stopped.
|
|
55
|
+
*/
|
|
56
|
+
export declare function buyerSideCommand(stream: {
|
|
57
|
+
readonly status: string;
|
|
58
|
+
readonly expiresAt: number;
|
|
59
|
+
}, streamId: string, nowSeconds: number): string;
|
|
45
60
|
//# sourceMappingURL=claim.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"claim.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/claim.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,QAAQ,EAA6B,MAAM,cAAc,CAAC;AASnE,OAAO,KAAK,EAAkB,WAAW,EAAE,MAAM,eAAe,CAAC;AACjE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAEhD;;;;;;;;;GASG;AACH,eAAO,MAAM,YAAY,EAAE,
|
|
1
|
+
{"version":3,"file":"claim.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/claim.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,QAAQ,EAA6B,MAAM,cAAc,CAAC;AASnE,OAAO,KAAK,EAAkB,WAAW,EAAE,MAAM,eAAe,CAAC;AACjE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAEhD;;;;;;;;;GASG;AACH,eAAO,MAAM,YAAY,EAAE,WAoH1B,CAAC;AAOF;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAGzD;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,MAAM,SAAS,GACjB;IACE,QAAQ,CAAC,IAAI,EAAE,gBAAgB,GAAG,aAAa,CAAC;IAChD,2DAA2D;IAC3D,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,0EAA0E;IAC1E,QAAQ,CAAC,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3C,6DAA6D;IAC7D,QAAQ,CAAC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1C,GACD;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAA;CAAE,CAAC;AAE3D,wBAAsB,SAAS,CAC7B,IAAI,EAAE,aAAa,EACnB,QAAQ,EAAE,KAAK,MAAM,EAAE,EACvB,MAAM,EAAE,IAAI,CAAC,QAAQ,EAAE,mBAAmB,CAAC,EAC3C,gBAAgB,EAAE,MAAM,GACvB,OAAO,CAAC,SAAS,CAAC,CA8FpB;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAAE,EAC/D,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,GACjB,MAAM,CAKR"}
|
|
@@ -49,34 +49,52 @@ export const claimCommand = {
|
|
|
49
49
|
if (stream.operator.toLowerCase() !== account.address.toLowerCase()) {
|
|
50
50
|
throw new CliError(`wallet ${account.address} is not the operator of this stream`, {
|
|
51
51
|
exitCode: 3,
|
|
52
|
-
hint: `only ${stream.operator} can claim it — the escrow enforces this, so no other wallet can. Set ${PRIVATE_KEY_ENV} to the operator's key. (The buyer's side of this stream is
|
|
52
|
+
hint: `only ${stream.operator} can claim it — the escrow enforces this, so no other wallet can. Set ${PRIVATE_KEY_ENV} to the operator's key. (The buyer's side of this stream is \`${buyerSideCommand(stream, streamId, deps.nowSeconds())}\`.)`,
|
|
53
53
|
});
|
|
54
54
|
}
|
|
55
|
-
const plan = await planClaim(read, streamId, stream, escrowGenerationFor(config.chainId, config.escrow));
|
|
56
|
-
if (plan.kind === "nothing")
|
|
57
|
-
throw plan.error;
|
|
58
55
|
const write = deps.createWriteClient(config, account, writeClientOptions(wallet));
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
spentSoFarUsdc: 0n,
|
|
66
|
-
nowSeconds: deps.nowSeconds(),
|
|
67
|
-
};
|
|
56
|
+
const generation = escrowGenerationFor(config.chainId, config.escrow);
|
|
57
|
+
// The oracle can post (or the escrow retain) a newer checkpoint between
|
|
58
|
+
// this plan and the transaction, so a proof read moments ago can revert
|
|
59
|
+
// `InvalidProof`. Nothing was spent on a reverted simulation: re-plan from
|
|
60
|
+
// fresh chain and proof state and try again, a bounded number of times.
|
|
61
|
+
let plan;
|
|
68
62
|
let result;
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
63
|
+
for (let attempt = 1;; attempt += 1) {
|
|
64
|
+
const planned = await planClaim(read, streamId, stream, generation);
|
|
65
|
+
if (planned.kind === "nothing")
|
|
66
|
+
throw planned.error;
|
|
67
|
+
plan = planned;
|
|
68
|
+
const action = {
|
|
69
|
+
streamId,
|
|
70
|
+
// `null` for a policy-enforced wallet — see `../spend-policy.ts`.
|
|
71
|
+
signedMandate: isPolicyEnforced(wallet)
|
|
72
|
+
? null
|
|
73
|
+
: await signClaimMandate(account, config.chainId),
|
|
74
|
+
spentSoFarUsdc: 0n,
|
|
75
|
+
nowSeconds: deps.nowSeconds(),
|
|
76
|
+
};
|
|
77
|
+
try {
|
|
78
|
+
if (plan.kind === "claimWithProof") {
|
|
79
|
+
result = await write.claimStream(action);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
if (write.claimProvenStream === undefined) {
|
|
83
|
+
throw new CliError("this write client cannot send claimProven, which is the only way to collect this stream's proven value", {
|
|
84
|
+
exitCode: 3,
|
|
85
|
+
hint: "no current checkpoint proof pays it, so a proof claim would revert NothingToClaim",
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
result = await write.claimProvenStream(action);
|
|
89
|
+
}
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
if (attempt >= CLAIM_REPLAN_ATTEMPTS || !isStaleProofError(error)) {
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
await deps.sleep(CLAIM_REPLAN_DELAY_MS);
|
|
78
97
|
}
|
|
79
|
-
result = await write.claimProvenStream(action);
|
|
80
98
|
}
|
|
81
99
|
out.result({
|
|
82
100
|
streamId,
|
|
@@ -102,6 +120,19 @@ export const claimCommand = {
|
|
|
102
120
|
]);
|
|
103
121
|
},
|
|
104
122
|
};
|
|
123
|
+
/** How many plan-then-send rounds `metrik claim` makes before giving up. */
|
|
124
|
+
const CLAIM_REPLAN_ATTEMPTS = 3;
|
|
125
|
+
/** Pause between rounds, so a just-posted checkpoint can settle on the RPC. */
|
|
126
|
+
const CLAIM_REPLAN_DELAY_MS = 3_000;
|
|
127
|
+
/**
|
|
128
|
+
* A claim reverted because its proof no longer matches a root the escrow
|
|
129
|
+
* accepts (a checkpoint rotated, or a lagging node served a not-yet-visible
|
|
130
|
+
* root). Re-planning reads a fresh proof, so it is worth one more round.
|
|
131
|
+
*/
|
|
132
|
+
export function isStaleProofError(error) {
|
|
133
|
+
const text = describeError(error);
|
|
134
|
+
return /InvalidProof|fresh checkpoint proof/.test(text);
|
|
135
|
+
}
|
|
105
136
|
export async function planClaim(read, streamId, stream, escrowGeneration) {
|
|
106
137
|
let verifiedCumulative = null;
|
|
107
138
|
let proofError = null;
|
|
@@ -187,4 +218,15 @@ export async function planClaim(read, streamId, stream, escrowGeneration) {
|
|
|
187
218
|
}),
|
|
188
219
|
};
|
|
189
220
|
}
|
|
221
|
+
/**
|
|
222
|
+
* The buyer's recovery command for a stream. A reclaim needs the clock
|
|
223
|
+
* stopped (closed, or past expiresAt), so a still-running stream gets
|
|
224
|
+
* `--close-first`; the plain form only once it has stopped.
|
|
225
|
+
*/
|
|
226
|
+
export function buyerSideCommand(stream, streamId, nowSeconds) {
|
|
227
|
+
const stopped = stream.status === "closed" || nowSeconds >= stream.expiresAt;
|
|
228
|
+
return stopped
|
|
229
|
+
? `metrik reclaim ${streamId}`
|
|
230
|
+
: `metrik reclaim ${streamId} --close-first`;
|
|
231
|
+
}
|
|
190
232
|
//# sourceMappingURL=claim.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"claim.js","sourceRoot":"","sources":["../../../src/cli/commands/claim.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC9D,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AACnE,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,EACL,gBAAgB,EAChB,eAAe,EACf,gBAAgB,GACjB,MAAM,cAAc,CAAC;AAGtB,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAGjE;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,YAAY,GAAgB;IACvC,IAAI,EAAE,OAAO;IACb,OAAO,EAAE,yDAAyD;IAClE,KAAK,EAAE,yBAAyB;IAChC,KAAK,EAAE,EAAE;IACT,KAAK,EAAE,IAAI;IACX,KAAK,CAAC,GAAG,CAAC,OAAuB;QAC/B,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC;QACjD,MAAM,CAAC,WAAW,CAAC,GAAG,IAAI,CAAC,WAAW,CAAC;QACvC,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;YAC9B,MAAM,UAAU,CAAC,oCAAoC,CAAC,CAAC;QACzD,CAAC;QACD,MAAM,QAAQ,GAAG,YAAY,CAAC,WAAW,EAAE,YAAY,CAAC,CAAC;QAEzD,MAAM,MAAM,GAAG,MAAM,gBAAgB,CAAC,GAAG,EAAE;YACzC,IAAI,EAAE,GAAG,CAAC,IAAI;YACd,MAAM,EAAE,MAAM,CAAC,MAAM;SACtB,CAAC,CAAC;QACH,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,CAAC;QAC3B,IAAI,gBAAgB,CAAC,MAAM,CAAC,EAAE,CAAC;YAC7B,kEAAkE;YAClE,kEAAkE;YAClE,qEAAqE;YACrE,oEAAoE;YACpE,qDAAqD;YACrD,GAAG,CAAC,IAAI,CACN,qJAAqJ,CACtJ,CAAC;QACJ,CAAC;QACD,MAAM,IAAI,GAAG,IAAI,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC;QAC3C,MAAM,gBAAgB,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC;QACxD,MAAM,eAAe,CAAC,IAAI,EAAE,OAAO,CAAC,OAAO,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;QAE7D,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC;QAChD,IAAI,MAAM,CAAC,QAAQ,CAAC,WAAW,EAAE,KAAK,OAAO,CAAC,OAAO,CAAC,WAAW,EAAE,EAAE,CAAC;YACpE,MAAM,IAAI,QAAQ,CAChB,UAAU,OAAO,CAAC,OAAO,qCAAqC,EAC9D;gBACE,QAAQ,EAAE,CAAC;gBACX,IAAI,EAAE,QAAQ,MAAM,CAAC,QAAQ,yEAAyE,eAAe,
|
|
1
|
+
{"version":3,"file":"claim.js","sourceRoot":"","sources":["../../../src/cli/commands/claim.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import type { X402QuoteSummary } from "@absol-labs/sdk/x402";
|
|
2
2
|
import { type ServerMandateCaps } from "../../mandates/env.js";
|
|
3
|
+
import { type CloseAfterOutcome } from "../../x402-buyer/refund-actions.js";
|
|
3
4
|
import type { CommandSpec } from "../context.js";
|
|
4
5
|
export declare const payCommand: CommandSpec;
|
|
6
|
+
/**
|
|
7
|
+
* The `closeAfter` part of `metrik pay --close-after --json`: what this
|
|
8
|
+
* client's close + reclaim did, with the amounts from the verified receipt.
|
|
9
|
+
*/
|
|
10
|
+
export declare function describeCloseAfter(outcome: CloseAfterOutcome, context: {
|
|
11
|
+
readonly price: bigint | null;
|
|
12
|
+
readonly deposit: bigint | null;
|
|
13
|
+
readonly finishCommand: string | null;
|
|
14
|
+
}): Record<string, unknown>;
|
|
5
15
|
/**
|
|
6
16
|
* One line stating what a quote commits the payer to, printed before signing:
|
|
7
17
|
* for an upfront-opener quote, the NON-REFUNDABLE per-call part and the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pay.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/pay.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAmB,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAE9E,OAAO,EAIL,KAAK,iBAAiB,EACvB,MAAM,uBAAuB,CAAC;AA2B/B,OAAO,KAAK,EAAkB,WAAW,EAAE,MAAM,eAAe,CAAC;AAOjE,eAAO,MAAM,UAAU,EAAE,
|
|
1
|
+
{"version":3,"file":"pay.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/pay.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAmB,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAE9E,OAAO,EAIL,KAAK,iBAAiB,EACvB,MAAM,uBAAuB,CAAC;AA2B/B,OAAO,EAEL,KAAK,iBAAiB,EACvB,MAAM,oCAAoC,CAAC;AAQ5C,OAAO,KAAK,EAAkB,WAAW,EAAE,MAAM,eAAe,CAAC;AAOjE,eAAO,MAAM,UAAU,EAAE,WA0ZxB,CAAC;AAEF;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,iBAAiB,EAC1B,OAAO,EAAE;IACP,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,QAAQ,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;CACvC,GACA,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAmDzB;AA2DD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,gBAAgB,GAAG,MAAM,CAQlE;AAqBD;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,eAAe,EAAE;IAAE,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,GAAG,SAAS,GACrE,KAAK,MAAM,EAAE,GAAG,IAAI,CAKtB;AAKD;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,iBAAiB,GAAG,IAAI,EACjC,KAAK,EAAE;IACL,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;IACvC,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;IACrC,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IACzC,QAAQ,CAAC,QAAQ,EAAE,KAAK,MAAM,EAAE,GAAG,SAAS,CAAC;CAC9C,GACA,iBAAiB,CA4CnB;AAqFD,0EAA0E;AAC1E,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAkDzD"}
|
package/dist/cli/commands/pay.js
CHANGED
|
@@ -7,6 +7,8 @@ import { CliError, configError, usageError } from "../errors.js";
|
|
|
7
7
|
import { usdcFundingHint } from "../../network.js";
|
|
8
8
|
import { CDP_ENV_VARS, PRIVATE_KEY_ENV, isPrivyWalletSource, resolveCliWallet, } from "../wallet.js";
|
|
9
9
|
import { hasHeader, parseHeaders } from "./invoke.js";
|
|
10
|
+
import { closeAfterPayment, } from "../../x402-buyer/refund-actions.js";
|
|
11
|
+
import { authorizeCloseSigner, DEFAULT_ATTEMPTS, DEFAULT_MAX_WAIT_SECONDS, explainRefundFailure, reclaimActivity, } from "./refund.js";
|
|
10
12
|
/** Default stream window a request-sized mandate allows (one day). */
|
|
11
13
|
const DEFAULT_MAX_DURATION_SECONDS = 86_400;
|
|
12
14
|
/** Longest response body printed in human mode before it is truncated. */
|
|
@@ -14,7 +16,7 @@ const MAX_PRINTED_BODY_CHARS = 64 * 1024;
|
|
|
14
16
|
export const payCommand = {
|
|
15
17
|
name: "pay",
|
|
16
18
|
summary: "Pay an x402 endpoint into a recoverable escrow stream, under a spend mandate",
|
|
17
|
-
usage: '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]',
|
|
19
|
+
usage: '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]',
|
|
18
20
|
flags: [
|
|
19
21
|
"data",
|
|
20
22
|
"json-body",
|
|
@@ -26,6 +28,7 @@ export const payCommand = {
|
|
|
26
28
|
"max-duration",
|
|
27
29
|
"min-duration",
|
|
28
30
|
"operator",
|
|
31
|
+
"close-after",
|
|
29
32
|
"units",
|
|
30
33
|
"allow-legacy-factory",
|
|
31
34
|
],
|
|
@@ -46,6 +49,7 @@ export const payCommand = {
|
|
|
46
49
|
"max-upfront": "--max-upfront <usdc> (cap on the NON-REFUNDABLE per-call part of an upfront-opener quote; default 0.10 USDC, 0 refuses any)",
|
|
47
50
|
data: "--data <body>",
|
|
48
51
|
"json-body": "--json <body> (alias: --json-body <body>)",
|
|
52
|
+
"close-after": "--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)",
|
|
49
53
|
},
|
|
50
54
|
async run(context) {
|
|
51
55
|
const { args, config, env, out, deps } = context;
|
|
@@ -154,7 +158,15 @@ export const payCommand = {
|
|
|
154
158
|
const opener = result.metrikStreamOpener;
|
|
155
159
|
const quote = result.quote;
|
|
156
160
|
const settlementTx = settlementTransaction(result.paymentResponse);
|
|
157
|
-
|
|
161
|
+
const closeAfter = booleanFlag(args, "close-after");
|
|
162
|
+
// A stream on another (e.g. legacy) registered escrow is reached by
|
|
163
|
+
// naming that escrow; the CLI defaults to the network's current one.
|
|
164
|
+
const escrowFlag = opener?.escrow !== null &&
|
|
165
|
+
opener?.escrow !== undefined &&
|
|
166
|
+
opener.escrow.toLowerCase() !== config.escrow.toLowerCase()
|
|
167
|
+
? ` --escrow ${opener.escrow}`
|
|
168
|
+
: "";
|
|
169
|
+
const payResult = {
|
|
158
170
|
paid: result.paid,
|
|
159
171
|
// The facilitator's settlement transaction, from the PAYMENT-RESPONSE
|
|
160
172
|
// header the seller relayed: the facilitator's relayer broadcast it, not
|
|
@@ -186,16 +198,36 @@ export const payCommand = {
|
|
|
186
198
|
minEarnSeconds: quote?.minEarnSeconds ?? null,
|
|
187
199
|
mandateSource: envCaps === null ? "request" : "env",
|
|
188
200
|
refundIsAutomatic: false,
|
|
189
|
-
}
|
|
201
|
+
};
|
|
190
202
|
if (!result.paid) {
|
|
203
|
+
// Nothing settled (the endpoint answered without a payment, or the paid
|
|
204
|
+
// request failed and was never settled): there is nothing to close.
|
|
205
|
+
out.result({
|
|
206
|
+
...payResult,
|
|
207
|
+
...(closeAfter ? { closeAfter: { outcome: "not-paid" } } : {}),
|
|
208
|
+
});
|
|
191
209
|
out.print(`The endpoint answered ${result.response.status} without asking for payment; nothing was signed or paid.`);
|
|
192
210
|
out.print(truncate(bodyText));
|
|
211
|
+
if (closeAfter) {
|
|
212
|
+
out.print("--close-after: nothing was paid, so there is nothing to close.");
|
|
213
|
+
}
|
|
193
214
|
return;
|
|
194
215
|
}
|
|
195
216
|
out.print(`Paid. HTTP ${result.response.status}`);
|
|
196
217
|
out.print(truncate(bodyText));
|
|
197
218
|
out.print("");
|
|
198
219
|
if (opener !== null && opener.streamId === null) {
|
|
220
|
+
out.result({
|
|
221
|
+
...payResult,
|
|
222
|
+
...(closeAfter
|
|
223
|
+
? {
|
|
224
|
+
closeAfter: {
|
|
225
|
+
outcome: "nothing-to-close",
|
|
226
|
+
refundIsAutomatic: false,
|
|
227
|
+
},
|
|
228
|
+
}
|
|
229
|
+
: {}),
|
|
230
|
+
});
|
|
199
231
|
// Upfront-only: no stream exists, so there is no refund or reclaim to offer.
|
|
200
232
|
out.table([
|
|
201
233
|
["opener", opener.opener],
|
|
@@ -212,64 +244,228 @@ export const payCommand = {
|
|
|
212
244
|
]);
|
|
213
245
|
out.print("");
|
|
214
246
|
out.print("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.");
|
|
247
|
+
if (closeAfter) {
|
|
248
|
+
out.print("--close-after: nothing to close — no stream was opened. Nothing was signed or sent.");
|
|
249
|
+
}
|
|
215
250
|
return;
|
|
216
251
|
}
|
|
217
|
-
if (opener
|
|
218
|
-
out.
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
[
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
252
|
+
if (opener === null || opener.streamId === null) {
|
|
253
|
+
out.result(payResult);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
const streamId = opener.streamId;
|
|
257
|
+
out.table([
|
|
258
|
+
["streamId", streamId],
|
|
259
|
+
["opener", opener.opener],
|
|
260
|
+
["payer", account.address],
|
|
261
|
+
...splitRows(quote, opener.upfrontFee),
|
|
262
|
+
...(opener.deposit === null
|
|
263
|
+
? []
|
|
264
|
+
: [["deposit", formatUsdc(opener.deposit)]]),
|
|
265
|
+
...(opener.ratePerSecond === null
|
|
266
|
+
? []
|
|
267
|
+
: [
|
|
268
|
+
["rate", `${formatUsdc(opener.ratePerSecond)} / verified second`],
|
|
269
|
+
]),
|
|
270
|
+
...(quote?.minEarnSeconds === null || quote?.minEarnSeconds === undefined
|
|
271
|
+
? []
|
|
272
|
+
: [
|
|
273
|
+
[
|
|
274
|
+
"fully earned after",
|
|
275
|
+
`${formatDuration(Number(quote.minEarnSeconds))} of verified delivery`,
|
|
276
|
+
],
|
|
277
|
+
]),
|
|
278
|
+
...(opener.expiresAt === null
|
|
279
|
+
? []
|
|
280
|
+
: [
|
|
281
|
+
["expires", new Date(opener.expiresAt * 1000).toISOString()],
|
|
282
|
+
]),
|
|
283
|
+
["refund url", opener.refundUrl ?? "(the seller advertised no relay)"],
|
|
284
|
+
...(settlementTx === null
|
|
285
|
+
? []
|
|
286
|
+
: [
|
|
287
|
+
[
|
|
288
|
+
"settlement tx",
|
|
289
|
+
`${settlementTx} (reported by the seller's facilitator in PAYMENT-RESPONSE; its relayer broadcast it)`,
|
|
290
|
+
],
|
|
291
|
+
]),
|
|
292
|
+
]);
|
|
293
|
+
out.print("");
|
|
294
|
+
const refundCommand = (flags) => opener.refundUrl === null
|
|
295
|
+
? `metrik reclaim ${streamId}${escrowFlag}`
|
|
296
|
+
: `metrik refund ${streamId} --refund-url ${opener.refundUrl}${escrowFlag} ${flags}`;
|
|
297
|
+
if (!closeAfter) {
|
|
298
|
+
out.result(payResult);
|
|
262
299
|
out.print(quote?.openerKind === "upfront" &&
|
|
263
300
|
quote.upfrontAmount !== null &&
|
|
264
301
|
quote.upfrontAmount > 0n
|
|
265
302
|
? `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:`
|
|
266
303
|
: "Refunds are NOT automatic. The seller earns only for verified seconds; whatever is unearned stays in the escrow until someone reclaims it for you:");
|
|
267
304
|
out.print(opener.refundUrl === null
|
|
268
|
-
? `
|
|
269
|
-
: `
|
|
305
|
+
? ` ${refundCommand("")} (anyone may send it; the refund always goes to the payer)`
|
|
306
|
+
: ` ${refundCommand("--close --reclaim")}`);
|
|
307
|
+
return;
|
|
270
308
|
}
|
|
309
|
+
// --close-after: the buyer asked for it, so THIS client now sends the
|
|
310
|
+
// buyer's own close and reclaim. Not an automatic refund.
|
|
311
|
+
out.note("--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.");
|
|
312
|
+
const outcome = await closeAfterPayment({
|
|
313
|
+
payment: opener,
|
|
314
|
+
payer: account.address,
|
|
315
|
+
openRefunds: async () => await x402.refundClient(config, {
|
|
316
|
+
refundUrl: opener.refundUrl,
|
|
317
|
+
streamId,
|
|
318
|
+
opener: opener.opener,
|
|
319
|
+
maxAttempts: DEFAULT_ATTEMPTS,
|
|
320
|
+
maxDelayMs: DEFAULT_MAX_WAIT_SECONDS * 1_000,
|
|
321
|
+
...(typeof opener.settledAt === "number"
|
|
322
|
+
? { settledAt: opener.settledAt }
|
|
323
|
+
: {}),
|
|
324
|
+
}),
|
|
325
|
+
closeSigner: async () => await authorizeCloseSigner(account, config.chainId),
|
|
326
|
+
sleep: deps.sleep,
|
|
327
|
+
note: out.note,
|
|
328
|
+
...(context.progress === undefined
|
|
329
|
+
? {}
|
|
330
|
+
: {
|
|
331
|
+
setActivity: (activity) => context.progress?.setActivity(activity),
|
|
332
|
+
}),
|
|
333
|
+
reclaimActivity: reclaimActivity(DEFAULT_ATTEMPTS, DEFAULT_MAX_WAIT_SECONDS),
|
|
334
|
+
});
|
|
335
|
+
const finishCommand = outcome.outcome === "incomplete"
|
|
336
|
+
? refundCommand(outcome.incomplete?.closed === true
|
|
337
|
+
? "--reclaim"
|
|
338
|
+
: "--close --reclaim")
|
|
339
|
+
: null;
|
|
340
|
+
out.result({
|
|
341
|
+
...payResult,
|
|
342
|
+
closeAfter: describeCloseAfter(outcome, {
|
|
343
|
+
price: quote?.amount ?? null,
|
|
344
|
+
deposit: opener.deposit,
|
|
345
|
+
finishCommand,
|
|
346
|
+
}),
|
|
347
|
+
});
|
|
348
|
+
out.table(closeAfterRows(outcome, quote, opener.deposit));
|
|
349
|
+
out.print("");
|
|
350
|
+
if (outcome.outcome === "incomplete") {
|
|
351
|
+
const incomplete = outcome.incomplete;
|
|
352
|
+
const why = incomplete?.error instanceof Error
|
|
353
|
+
? explainRefundFailure(incomplete.error)
|
|
354
|
+
: null;
|
|
355
|
+
throw new CliError(`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"}`, {
|
|
356
|
+
exitCode: 5,
|
|
357
|
+
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")}${why instanceof CliError && why.hint !== undefined
|
|
358
|
+
? `\n (${why.hint})`
|
|
359
|
+
: ""}`,
|
|
360
|
+
cause: incomplete?.error ?? undefined,
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
out.print(describeCloseAfterDone(outcome));
|
|
271
364
|
},
|
|
272
365
|
};
|
|
366
|
+
/**
|
|
367
|
+
* The `closeAfter` part of `metrik pay --close-after --json`: what this
|
|
368
|
+
* client's close + reclaim did, with the amounts from the verified receipt.
|
|
369
|
+
*/
|
|
370
|
+
export function describeCloseAfter(outcome, context) {
|
|
371
|
+
const refunded = outcome.refundedUsdc;
|
|
372
|
+
return {
|
|
373
|
+
outcome: outcome.outcome,
|
|
374
|
+
streamId: outcome.streamId,
|
|
375
|
+
close: outcome.close === null
|
|
376
|
+
? null
|
|
377
|
+
: {
|
|
378
|
+
transaction: outcome.close.transaction,
|
|
379
|
+
verified: outcome.close.verified,
|
|
380
|
+
},
|
|
381
|
+
closeSkipped: outcome.closeSkipped,
|
|
382
|
+
reclaim: outcome.reclaim === null
|
|
383
|
+
? null
|
|
384
|
+
: {
|
|
385
|
+
transaction: outcome.reclaim.transaction,
|
|
386
|
+
amount: outcome.reclaim.amount,
|
|
387
|
+
verified: outcome.reclaim.verified,
|
|
388
|
+
},
|
|
389
|
+
reclaimSkipped: outcome.reclaimSkipped,
|
|
390
|
+
// Base units returned to the payer, from the receipt's ReclaimForwarded
|
|
391
|
+
// (null unless the receipt was verified).
|
|
392
|
+
refunded,
|
|
393
|
+
// What the payment finally cost: price - refunded (upfront + earned).
|
|
394
|
+
kept: refunded === null || context.price === null
|
|
395
|
+
? null
|
|
396
|
+
: context.price - refunded,
|
|
397
|
+
// What the seller earned from the escrow: deposit - refunded.
|
|
398
|
+
earned: refunded === null || context.deposit === null
|
|
399
|
+
? null
|
|
400
|
+
: context.deposit - refunded,
|
|
401
|
+
finishedReason: outcome.finished,
|
|
402
|
+
status: outcome.status?.status ?? null,
|
|
403
|
+
reclaimed: outcome.status?.reclaimed ?? null,
|
|
404
|
+
incomplete: outcome.incomplete === null
|
|
405
|
+
? null
|
|
406
|
+
: {
|
|
407
|
+
stage: outcome.incomplete.stage,
|
|
408
|
+
closed: outcome.incomplete.closed,
|
|
409
|
+
reason: outcome.incomplete.reason,
|
|
410
|
+
},
|
|
411
|
+
finishCommand: context.finishCommand,
|
|
412
|
+
// The buyer's own client sent these, at the buyer's request.
|
|
413
|
+
sentBy: "buyer",
|
|
414
|
+
refundIsAutomatic: false,
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
/** Human rows for a close-after run. */
|
|
418
|
+
function closeAfterRows(outcome, quote, deposit) {
|
|
419
|
+
const rows = [];
|
|
420
|
+
if (outcome.close !== null) {
|
|
421
|
+
rows.push([
|
|
422
|
+
"close tx",
|
|
423
|
+
`${outcome.close.transaction}${outcome.close.verified ? " (verified)" : " (relay-reported)"}`,
|
|
424
|
+
]);
|
|
425
|
+
}
|
|
426
|
+
else if (outcome.closeSkipped !== null) {
|
|
427
|
+
rows.push(["close", "skipped — already closed"]);
|
|
428
|
+
}
|
|
429
|
+
if (outcome.reclaim !== null) {
|
|
430
|
+
rows.push([
|
|
431
|
+
"reclaim tx",
|
|
432
|
+
`${outcome.reclaim.transaction}${outcome.reclaim.verified ? " (verified)" : " (relay-reported)"}`,
|
|
433
|
+
]);
|
|
434
|
+
const amount = outcome.refundedUsdc ?? outcome.reclaim.amount;
|
|
435
|
+
if (amount !== null) {
|
|
436
|
+
rows.push([
|
|
437
|
+
"refunded to payer",
|
|
438
|
+
`${formatUsdc(amount)}${outcome.refundedUsdc === null ? " (relay-reported; the receipt was NOT verified)" : " (from the verified receipt)"}`,
|
|
439
|
+
]);
|
|
440
|
+
if (deposit !== null) {
|
|
441
|
+
rows.push([
|
|
442
|
+
"seller earned",
|
|
443
|
+
`${formatUsdc(deposit - amount)} (verified seconds)`,
|
|
444
|
+
]);
|
|
445
|
+
}
|
|
446
|
+
if (quote !== null) {
|
|
447
|
+
rows.push([
|
|
448
|
+
"payment cost",
|
|
449
|
+
`${formatUsdc(quote.amount - amount)} of the ${formatUsdc(quote.amount)} paid`,
|
|
450
|
+
]);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
return rows;
|
|
455
|
+
}
|
|
456
|
+
function describeCloseAfterDone(outcome) {
|
|
457
|
+
if (outcome.reclaim !== null) {
|
|
458
|
+
const amount = outcome.refundedUsdc ?? outcome.reclaim.amount;
|
|
459
|
+
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.`;
|
|
460
|
+
}
|
|
461
|
+
if (outcome.finished === "reclaimed") {
|
|
462
|
+
return "Already finished: the stream is closed and was already reclaimed to the payer. Nothing further to do.";
|
|
463
|
+
}
|
|
464
|
+
if (outcome.finished === "nothing-to-reclaim") {
|
|
465
|
+
return "Closed at your request. Nothing to reclaim: the verified delivery covers the whole deposit, so none of it is unearned.";
|
|
466
|
+
}
|
|
467
|
+
return "Closed at your request.";
|
|
468
|
+
}
|
|
273
469
|
/**
|
|
274
470
|
* One line stating what a quote commits the payer to, printed before signing:
|
|
275
471
|
* for an upfront-opener quote, the NON-REFUNDABLE per-call part and the
|