@absol-labs/agent 0.9.1 → 0.11.0
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 +184 -26
- package/dist/capability/invocation-capability.d.ts +18 -6
- package/dist/capability/invocation-capability.d.ts.map +1 -1
- package/dist/capability/invocation-capability.js.map +1 -1
- package/dist/cli/amounts.d.ts +26 -0
- package/dist/cli/amounts.d.ts.map +1 -0
- package/dist/cli/amounts.js +71 -0
- package/dist/cli/amounts.js.map +1 -0
- package/dist/cli/args.d.ts +32 -0
- package/dist/cli/args.d.ts.map +1 -0
- package/dist/cli/args.js +119 -0
- package/dist/cli/args.js.map +1 -0
- package/dist/cli/attesters.d.ts +51 -0
- package/dist/cli/attesters.d.ts.map +1 -0
- package/dist/cli/attesters.js +58 -0
- package/dist/cli/attesters.js.map +1 -0
- package/dist/cli/bin.d.ts +3 -0
- package/dist/cli/bin.d.ts.map +1 -0
- package/dist/cli/bin.js +5 -0
- package/dist/cli/bin.js.map +1 -0
- package/dist/cli/commands/claim.d.ts +13 -0
- package/dist/cli/commands/claim.d.ts.map +1 -0
- package/dist/cli/commands/claim.js +102 -0
- package/dist/cli/commands/claim.js.map +1 -0
- package/dist/cli/commands/close.d.ts +3 -0
- package/dist/cli/commands/close.d.ts.map +1 -0
- package/dist/cli/commands/close.js +55 -0
- package/dist/cli/commands/close.js.map +1 -0
- package/dist/cli/commands/discover.d.ts +3 -0
- package/dist/cli/commands/discover.d.ts.map +1 -0
- package/dist/cli/commands/discover.js +183 -0
- package/dist/cli/commands/discover.js.map +1 -0
- package/dist/cli/commands/invoke.d.ts +3 -0
- package/dist/cli/commands/invoke.d.ts.map +1 -0
- package/dist/cli/commands/invoke.js +142 -0
- package/dist/cli/commands/invoke.js.map +1 -0
- package/dist/cli/commands/mcp.d.ts +3 -0
- package/dist/cli/commands/mcp.d.ts.map +1 -0
- package/dist/cli/commands/mcp.js +113 -0
- package/dist/cli/commands/mcp.js.map +1 -0
- package/dist/cli/commands/open.d.ts +3 -0
- package/dist/cli/commands/open.d.ts.map +1 -0
- package/dist/cli/commands/open.js +331 -0
- package/dist/cli/commands/open.js.map +1 -0
- package/dist/cli/commands/reclaim-unverified.d.ts +3 -0
- package/dist/cli/commands/reclaim-unverified.d.ts.map +1 -0
- package/dist/cli/commands/reclaim-unverified.js +172 -0
- package/dist/cli/commands/reclaim-unverified.js.map +1 -0
- package/dist/cli/commands/reclaim.d.ts +33 -0
- package/dist/cli/commands/reclaim.d.ts.map +1 -0
- package/dist/cli/commands/reclaim.js +308 -0
- package/dist/cli/commands/reclaim.js.map +1 -0
- package/dist/cli/commands/status.d.ts +3 -0
- package/dist/cli/commands/status.d.ts.map +1 -0
- package/dist/cli/commands/status.js +43 -0
- package/dist/cli/commands/status.js.map +1 -0
- package/dist/cli/commands/streams.d.ts +3 -0
- package/dist/cli/commands/streams.d.ts.map +1 -0
- package/dist/cli/commands/streams.js +257 -0
- package/dist/cli/commands/streams.js.map +1 -0
- package/dist/cli/commands/watch.d.ts +3 -0
- package/dist/cli/commands/watch.d.ts.map +1 -0
- package/dist/cli/commands/watch.js +55 -0
- package/dist/cli/commands/watch.js.map +1 -0
- package/dist/cli/config.d.ts +61 -0
- package/dist/cli/config.d.ts.map +1 -0
- package/dist/cli/config.js +140 -0
- package/dist/cli/config.js.map +1 -0
- package/dist/cli/context.d.ts +29 -0
- package/dist/cli/context.d.ts.map +1 -0
- package/dist/cli/context.js +2 -0
- package/dist/cli/context.js.map +1 -0
- package/dist/cli/deps.d.ts +191 -0
- package/dist/cli/deps.d.ts.map +1 -0
- package/dist/cli/deps.js +277 -0
- package/dist/cli/deps.js.map +1 -0
- package/dist/cli/descriptor.d.ts +61 -0
- package/dist/cli/descriptor.d.ts.map +1 -0
- package/dist/cli/descriptor.js +78 -0
- package/dist/cli/descriptor.js.map +1 -0
- package/dist/cli/errors.d.ts +48 -0
- package/dist/cli/errors.d.ts.map +1 -0
- package/dist/cli/errors.js +129 -0
- package/dist/cli/errors.js.map +1 -0
- package/dist/cli/escape-hatch.d.ts +50 -0
- package/dist/cli/escape-hatch.d.ts.map +1 -0
- package/dist/cli/escape-hatch.js +45 -0
- package/dist/cli/escape-hatch.js.map +1 -0
- package/dist/cli/index.d.ts +8 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +7 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/listings.d.ts +15 -0
- package/dist/cli/listings.d.ts.map +1 -0
- package/dist/cli/listings.js +56 -0
- package/dist/cli/listings.js.map +1 -0
- package/dist/cli/mandate.d.ts +66 -0
- package/dist/cli/mandate.d.ts.map +1 -0
- package/dist/cli/mandate.js +61 -0
- package/dist/cli/mandate.js.map +1 -0
- package/dist/cli/output.d.ts +33 -0
- package/dist/cli/output.d.ts.map +1 -0
- package/dist/cli/output.js +34 -0
- package/dist/cli/output.js.map +1 -0
- package/dist/cli/preflight.d.ts +38 -0
- package/dist/cli/preflight.d.ts.map +1 -0
- package/dist/cli/preflight.js +72 -0
- package/dist/cli/preflight.js.map +1 -0
- package/dist/cli/privy.d.ts +83 -0
- package/dist/cli/privy.d.ts.map +1 -0
- package/dist/cli/privy.js +230 -0
- package/dist/cli/privy.js.map +1 -0
- package/dist/cli/progress.d.ts +36 -0
- package/dist/cli/progress.d.ts.map +1 -0
- package/dist/cli/progress.js +40 -0
- package/dist/cli/progress.js.map +1 -0
- package/dist/cli/run.d.ts +25 -0
- package/dist/cli/run.d.ts.map +1 -0
- package/dist/cli/run.js +173 -0
- package/dist/cli/run.js.map +1 -0
- package/dist/cli/scan-range.d.ts +55 -0
- package/dist/cli/scan-range.d.ts.map +1 -0
- package/dist/cli/scan-range.js +57 -0
- package/dist/cli/scan-range.js.map +1 -0
- package/dist/cli/spend-policy.d.ts +60 -0
- package/dist/cli/spend-policy.d.ts.map +1 -0
- package/dist/cli/spend-policy.js +65 -0
- package/dist/cli/spend-policy.js.map +1 -0
- package/dist/cli/status.d.ts +90 -0
- package/dist/cli/status.d.ts.map +1 -0
- package/dist/cli/status.js +252 -0
- package/dist/cli/status.js.map +1 -0
- package/dist/cli/timeout.d.ts +30 -0
- package/dist/cli/timeout.d.ts.map +1 -0
- package/dist/cli/timeout.js +55 -0
- package/dist/cli/timeout.js.map +1 -0
- package/dist/cli/version.d.ts +12 -0
- package/dist/cli/version.d.ts.map +1 -0
- package/dist/cli/version.js +22 -0
- package/dist/cli/version.js.map +1 -0
- package/dist/cli/wallet.d.ts +164 -0
- package/dist/cli/wallet.d.ts.map +1 -0
- package/dist/cli/wallet.js +234 -0
- package/dist/cli/wallet.js.map +1 -0
- package/dist/frameworks/crewai.d.ts +10 -0
- package/dist/frameworks/crewai.d.ts.map +1 -1
- package/dist/frameworks/crewai.js +12 -2
- package/dist/frameworks/crewai.js.map +1 -1
- package/dist/gateway/caller-auth-gateway.d.ts +38 -3
- package/dist/gateway/caller-auth-gateway.d.ts.map +1 -1
- package/dist/gateway/caller-auth-gateway.js +25 -1
- package/dist/gateway/caller-auth-gateway.js.map +1 -1
- package/dist/gateway/http-server.d.ts +53 -0
- package/dist/gateway/http-server.d.ts.map +1 -1
- package/dist/gateway/http-server.js +189 -4
- package/dist/gateway/http-server.js.map +1 -1
- package/dist/gateway/server-entry.d.ts +1 -0
- package/dist/gateway/server-entry.js +1 -0
- package/dist/gateway/server-entry.js.map +1 -1
- package/dist/gateway/x402-mode.d.ts +123 -0
- package/dist/gateway/x402-mode.d.ts.map +1 -0
- package/dist/gateway/x402-mode.js +212 -0
- package/dist/gateway/x402-mode.js.map +1 -0
- package/dist/index.d.ts +6 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -3
- package/dist/index.js.map +1 -1
- package/dist/mandates/env.d.ts +3 -2
- package/dist/mandates/env.d.ts.map +1 -1
- package/dist/mandates/env.js +3 -2
- package/dist/mandates/env.js.map +1 -1
- package/dist/mcp/http-server.d.ts +1 -0
- package/dist/mcp/http-server.js +1 -0
- package/dist/mcp/http-server.js.map +1 -1
- package/dist/mcp/server.d.ts +49 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +533 -25
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/service-invoker.d.ts +97 -0
- package/dist/mcp/service-invoker.d.ts.map +1 -0
- package/dist/mcp/service-invoker.js +167 -0
- package/dist/mcp/service-invoker.js.map +1 -0
- package/dist/mcp/stdio.d.ts +1 -0
- package/dist/mcp/stdio.js +1 -0
- package/dist/mcp/stdio.js.map +1 -1
- package/dist/sdk/client.d.ts +25 -1
- package/dist/sdk/client.d.ts.map +1 -1
- package/dist/sdk/client.js +20 -1
- package/dist/sdk/client.js.map +1 -1
- package/dist/sdk/invoke.d.ts +19 -4
- package/dist/sdk/invoke.d.ts.map +1 -1
- package/dist/sdk/invoke.js +69 -5
- package/dist/sdk/invoke.js.map +1 -1
- package/dist/x402/delivery-oracle.d.ts +108 -0
- package/dist/x402/delivery-oracle.d.ts.map +1 -0
- package/dist/x402/delivery-oracle.js +174 -0
- package/dist/x402/delivery-oracle.js.map +1 -0
- package/dist/x402/facilitator-server-entry.d.ts +3 -0
- package/dist/x402/facilitator-server-entry.d.ts.map +1 -0
- package/dist/x402/facilitator-server-entry.js +27 -0
- package/dist/x402/facilitator-server-entry.js.map +1 -0
- package/dist/x402/facilitator-server.d.ts +73 -0
- package/dist/x402/facilitator-server.d.ts.map +1 -0
- package/dist/x402/facilitator-server.js +305 -0
- package/dist/x402/facilitator-server.js.map +1 -0
- package/dist/x402/facilitator.d.ts +69 -17
- package/dist/x402/facilitator.d.ts.map +1 -1
- package/dist/x402/facilitator.js +76 -33
- package/dist/x402/facilitator.js.map +1 -1
- package/dist/x402/payment-requirements.d.ts +106 -0
- package/dist/x402/payment-requirements.d.ts.map +1 -0
- package/dist/x402/payment-requirements.js +138 -0
- package/dist/x402/payment-requirements.js.map +1 -0
- package/dist/x402/relayer.d.ts +41 -0
- package/dist/x402/relayer.d.ts.map +1 -0
- package/dist/x402/relayer.js +64 -0
- package/dist/x402/relayer.js.map +1 -0
- package/dist/x402/resource-index.d.ts +75 -0
- package/dist/x402/resource-index.d.ts.map +1 -0
- package/dist/x402/resource-index.js +126 -0
- package/dist/x402/resource-index.js.map +1 -0
- package/dist/x402/verified-delivery-facilitator.d.ts +285 -0
- package/dist/x402/verified-delivery-facilitator.d.ts.map +1 -0
- package/dist/x402/verified-delivery-facilitator.js +423 -0
- package/dist/x402/verified-delivery-facilitator.js.map +1 -0
- package/dist/zktls/t2-delivery-proof.d.ts +6 -6
- package/docs/agent-layer.md +14 -9
- package/docs/cli.md +705 -0
- package/docs/crewai.md +6 -2
- package/docs/eliza.md +6 -1
- package/docs/mcp-hosted.md +7 -5
- package/docs/mcp-reference.md +175 -0
- package/docs/quickstart.md +36 -9
- package/docs/threat-model.md +4 -3
- package/docs/x402.md +648 -0
- package/package.json +33 -10
- package/src/capability/invocation-capability.ts +17 -1
- package/src/cli/amounts.ts +102 -0
- package/src/cli/args.ts +150 -0
- package/src/cli/attesters.ts +86 -0
- package/src/cli/bin.ts +5 -0
- package/src/cli/commands/claim.ts +126 -0
- package/src/cli/commands/close.ts +69 -0
- package/src/cli/commands/discover.ts +228 -0
- package/src/cli/commands/invoke.ts +178 -0
- package/src/cli/commands/mcp.ts +135 -0
- package/src/cli/commands/open.ts +397 -0
- package/src/cli/commands/reclaim-unverified.ts +218 -0
- package/src/cli/commands/reclaim.ts +377 -0
- package/src/cli/commands/status.ts +56 -0
- package/src/cli/commands/streams.ts +344 -0
- package/src/cli/commands/watch.ts +70 -0
- package/src/cli/config.ts +208 -0
- package/src/cli/context.ts +30 -0
- package/src/cli/deps.ts +528 -0
- package/src/cli/descriptor.ts +133 -0
- package/src/cli/errors.ts +162 -0
- package/src/cli/escape-hatch.ts +92 -0
- package/src/cli/index.ts +27 -0
- package/src/cli/listings.ts +77 -0
- package/src/cli/mandate.ts +126 -0
- package/src/cli/output.ts +63 -0
- package/src/cli/preflight.ts +88 -0
- package/src/cli/privy.ts +396 -0
- package/src/cli/progress.ts +75 -0
- package/src/cli/run.ts +209 -0
- package/src/cli/scan-range.ts +76 -0
- package/src/cli/spend-policy.ts +139 -0
- package/src/cli/status.ts +381 -0
- package/src/cli/timeout.ts +64 -0
- package/src/cli/version.ts +21 -0
- package/src/cli/wallet.ts +372 -0
- package/src/frameworks/crewai.ts +13 -2
- package/src/gateway/caller-auth-gateway.ts +77 -6
- package/src/gateway/http-server.ts +280 -6
- package/src/gateway/server-entry.ts +1 -0
- package/src/gateway/x402-mode.ts +350 -0
- package/src/index.ts +40 -5
- package/src/mandates/env.ts +3 -2
- package/src/mcp/http-server.ts +1 -0
- package/src/mcp/server.ts +706 -30
- package/src/mcp/service-invoker.ts +285 -0
- package/src/mcp/stdio.ts +1 -0
- package/src/sdk/client.ts +33 -1
- package/src/sdk/invoke.ts +90 -12
- package/src/x402/delivery-oracle.ts +285 -0
- package/src/x402/facilitator-server-entry.ts +35 -0
- package/src/x402/facilitator-server.ts +445 -0
- package/src/x402/facilitator.ts +125 -46
- package/src/x402/payment-requirements.ts +231 -0
- package/src/x402/relayer.ts +122 -0
- package/src/x402/resource-index.ts +228 -0
- package/src/x402/verified-delivery-facilitator.ts +744 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { SettlementWindowOpenError } from "@absol-labs/sdk";
|
|
2
|
+
import { booleanFlag, integerFlag } from "../args.js";
|
|
3
|
+
import { formatUsdc } from "../amounts.js";
|
|
4
|
+
import { assertTestnetRpc, parseBytes32 } from "../config.js";
|
|
5
|
+
import { CliError, describeError, usageError } from "../errors.js";
|
|
6
|
+
import { describeEscapeHatch, escapeHatchWindow, readChainNow, } from "../escape-hatch.js";
|
|
7
|
+
import { signRecoveryMandate } from "../mandate.js";
|
|
8
|
+
import { assertCanPayGas } from "../preflight.js";
|
|
9
|
+
import { writeClientOptions } from "../deps.js";
|
|
10
|
+
import { isPolicyEnforced, resolveCliWallet } from "../wallet.js";
|
|
11
|
+
/**
|
|
12
|
+
* How long to keep retrying a reclaim that is blocked only by an open
|
|
13
|
+
* settlement window. Defaults chosen to comfortably cover the oracle's ~15-22s
|
|
14
|
+
* checkpoint cadence: 12 x 10s = two minutes.
|
|
15
|
+
*/
|
|
16
|
+
const DEFAULT_ATTEMPTS = 12;
|
|
17
|
+
const DEFAULT_INTERVAL_SECONDS = 10;
|
|
18
|
+
export const reclaimCommand = {
|
|
19
|
+
name: "reclaim",
|
|
20
|
+
summary: "Take back the budget that was never earned",
|
|
21
|
+
usage: "metrik reclaim <streamId> [--close-first] [--attempts 12] [--interval 10]",
|
|
22
|
+
flags: ["close-first", "attempts", "interval"],
|
|
23
|
+
signs: true,
|
|
24
|
+
async run(context) {
|
|
25
|
+
const { args, config, env, out, deps } = context;
|
|
26
|
+
const [streamIdArg] = args.positionals;
|
|
27
|
+
if (streamIdArg === undefined) {
|
|
28
|
+
throw usageError("metrik reclaim requires a <streamId>");
|
|
29
|
+
}
|
|
30
|
+
const streamId = parseBytes32(streamIdArg, "<streamId>");
|
|
31
|
+
const closeFirst = booleanFlag(args, "close-first");
|
|
32
|
+
const attempts = integerFlag(args, "attempts") ?? DEFAULT_ATTEMPTS;
|
|
33
|
+
const interval = integerFlag(args, "interval") ?? DEFAULT_INTERVAL_SECONDS;
|
|
34
|
+
if (attempts < 1)
|
|
35
|
+
throw usageError("--attempts must be at least 1");
|
|
36
|
+
if (interval < 1)
|
|
37
|
+
throw usageError("--interval must be at least 1 second");
|
|
38
|
+
const wallet = await resolveCliWallet(env, {
|
|
39
|
+
note: out.note,
|
|
40
|
+
rpcUrl: config.rpcUrl,
|
|
41
|
+
});
|
|
42
|
+
const { account } = wallet;
|
|
43
|
+
const read = deps.createReadClient(config);
|
|
44
|
+
await assertTestnetRpc(config, () => read.getChainId());
|
|
45
|
+
await assertCanPayGas(read, account.address);
|
|
46
|
+
const write = deps.createWriteClient(config, account, writeClientOptions(wallet));
|
|
47
|
+
// A policy-enforced wallet (Privy) signs no mandate of its own: these are
|
|
48
|
+
// buyer-recovery actions, gated by the escrow on the buyer address and by
|
|
49
|
+
// the broker on `stream.buyer == wallet`. See `../spend-policy.ts`.
|
|
50
|
+
const recoveryMandate = async () => isPolicyEnforced(wallet)
|
|
51
|
+
? null
|
|
52
|
+
: await signRecoveryMandate(account, config.chainId);
|
|
53
|
+
// `--close-first` is TWO transactions, and the second one only simulates
|
|
54
|
+
// successfully against post-close state. Sequencing them inside a single SDK
|
|
55
|
+
// call sent the reclaim simulation at a node that had not yet applied the
|
|
56
|
+
// close, and the command died with `StreamNotReclaimable()` having already
|
|
57
|
+
// spent the buyer's close — leaving the stream closed-but-unreclaimed, and
|
|
58
|
+
// the obvious retry dead-ending on `StreamClosedAlready()`. So the CLI owns
|
|
59
|
+
// the ordering: skip the close when the stream is already closed, and wait
|
|
60
|
+
// for the close to be VISIBLE to the node we are about to simulate against.
|
|
61
|
+
let closeResult = null;
|
|
62
|
+
let alreadyClosed = false;
|
|
63
|
+
if (closeFirst) {
|
|
64
|
+
const before = await read.getStreamV2(streamId).catch(() => null);
|
|
65
|
+
if (before !== null && before.status === "closed") {
|
|
66
|
+
alreadyClosed = true;
|
|
67
|
+
out.note("stream is already closed — skipping the close and going straight to reclaim.");
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
closeResult = await closeBeforeReclaim(write, {
|
|
71
|
+
streamId,
|
|
72
|
+
signedMandate: await recoveryMandate(),
|
|
73
|
+
spentSoFarUsdc: 0n,
|
|
74
|
+
nowSeconds: deps.nowSeconds(),
|
|
75
|
+
});
|
|
76
|
+
if (closeResult === null) {
|
|
77
|
+
alreadyClosed = true;
|
|
78
|
+
out.note("the stream was already closed by the time the close was sent — going straight to reclaim.");
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
out.note(`closed in ${closeResult.txHash} — waiting for it to land …`);
|
|
82
|
+
await awaitClosed(read, streamId, deps.sleep, closeResult.txHash);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const result = await reclaimWithBoundedRetry(write, async () => ({
|
|
87
|
+
streamId,
|
|
88
|
+
signedMandate: await recoveryMandate(),
|
|
89
|
+
spentSoFarUsdc: 0n,
|
|
90
|
+
nowSeconds: deps.nowSeconds(),
|
|
91
|
+
}), {
|
|
92
|
+
attempts,
|
|
93
|
+
intervalMs: interval * 1_000,
|
|
94
|
+
sleep: deps.sleep,
|
|
95
|
+
onRetry: (attempt, reason) => out.note(`reclaim blocked: no checkpoint covers the stream's stop time yet (attempt ${attempt}/${attempts}) — ${reason}`),
|
|
96
|
+
// "Retry later" is the wrong advice when the oracle is gone: every
|
|
97
|
+
// attempt this command makes needs a proof FROM that oracle. So the
|
|
98
|
+
// give-up message names the path that needs no proof, and says whether
|
|
99
|
+
// it is open yet.
|
|
100
|
+
exhaustedHint: async () => await describeLastResort(read, streamId, deps.nowSeconds()),
|
|
101
|
+
});
|
|
102
|
+
// Read the settled figure back only once the node we are reading from has
|
|
103
|
+
// actually applied the reclaim. `sepolia.base.org` is load-balanced, so an
|
|
104
|
+
// immediate read can land on a lagged node and report `settledCumulative: 0`
|
|
105
|
+
// for a stream that just settled a non-zero amount — printing "settled to
|
|
106
|
+
// operator 0 USDC" when it was not zero. Poll for `reclaimed` (which the
|
|
107
|
+
// transaction we just landed sets) so the number beside it is from the same
|
|
108
|
+
// post-reclaim state. Bounded, and falls back to the last value read.
|
|
109
|
+
const stream = await readSettledStream(read, streamId, deps.sleep);
|
|
110
|
+
out.result({
|
|
111
|
+
streamId,
|
|
112
|
+
txHash: result.reclaimResult.txHash,
|
|
113
|
+
closeTxHash: closeResult?.txHash ?? null,
|
|
114
|
+
// `--close-first` on a stream that was already closed is a no-op, not a
|
|
115
|
+
// failure. Machine consumers need to tell that apart from "no close was
|
|
116
|
+
// asked for".
|
|
117
|
+
closeSkippedAlreadyClosed: closeFirst ? alreadyClosed : null,
|
|
118
|
+
settledCumulative: stream?.settledCumulative ?? null,
|
|
119
|
+
});
|
|
120
|
+
out.print("Reclaimed.");
|
|
121
|
+
out.table([
|
|
122
|
+
["streamId", streamId],
|
|
123
|
+
["reclaim tx", result.reclaimResult.txHash],
|
|
124
|
+
...(closeResult === null
|
|
125
|
+
? alreadyClosed
|
|
126
|
+
? [["close", "skipped — already closed"]]
|
|
127
|
+
: []
|
|
128
|
+
: [["close tx", closeResult.txHash]]),
|
|
129
|
+
...(stream === null
|
|
130
|
+
? []
|
|
131
|
+
: [
|
|
132
|
+
["settled to operator", formatUsdc(stream.settledCumulative)],
|
|
133
|
+
]),
|
|
134
|
+
]);
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
/**
|
|
138
|
+
* Send the close that `--close-first` asks for, tolerating the one race that
|
|
139
|
+
* matters: a stale read said "active" but the stream was in fact already closed.
|
|
140
|
+
* That is the state the caller wanted, so it is reported as `null` (no close
|
|
141
|
+
* sent) rather than as a failure — re-running the command must never dead-end on
|
|
142
|
+
* `StreamClosedAlready()` while the buyer's money is still reclaimable.
|
|
143
|
+
*
|
|
144
|
+
* Every OTHER close failure is rethrown untouched.
|
|
145
|
+
*/
|
|
146
|
+
export async function closeBeforeReclaim(write, input) {
|
|
147
|
+
try {
|
|
148
|
+
return await write.closeStream(input);
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
if (isAlreadyClosed(error))
|
|
152
|
+
return null;
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/** True when `error`, or anything in its cause chain, is StreamClosedAlready. */
|
|
157
|
+
function isAlreadyClosed(error) {
|
|
158
|
+
for (let current = error, depth = 0; depth < 8; depth++) {
|
|
159
|
+
if (!(current instanceof Error))
|
|
160
|
+
return false;
|
|
161
|
+
if (current.message.includes("StreamClosedAlready"))
|
|
162
|
+
return true;
|
|
163
|
+
if (current.cause === undefined)
|
|
164
|
+
return false;
|
|
165
|
+
current = current.cause;
|
|
166
|
+
}
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Block until the node we read from reports the stream as closed.
|
|
171
|
+
*
|
|
172
|
+
* The close transaction's own receipt is awaited inside the SDK, but
|
|
173
|
+
* `sepolia.base.org` is load-balanced: the receipt can come back from one node
|
|
174
|
+
* while the very next `simulateContract` lands on another that has not imported
|
|
175
|
+
* the block yet. That node simulates `reclaim` against PRE-close state and
|
|
176
|
+
* reverts `StreamNotReclaimable()`. Confirming the close through the same read
|
|
177
|
+
* path the reclaim will use is what closes that window.
|
|
178
|
+
*/
|
|
179
|
+
const CLOSE_VISIBLE_ATTEMPTS = 20;
|
|
180
|
+
const CLOSE_VISIBLE_INTERVAL_MS = 500;
|
|
181
|
+
export async function awaitClosed(read, streamId, sleep, closeTxHash) {
|
|
182
|
+
for (let attempt = 1; attempt <= CLOSE_VISIBLE_ATTEMPTS; attempt += 1) {
|
|
183
|
+
const stream = await read.getStreamV2(streamId).catch(() => null);
|
|
184
|
+
if (stream !== null && stream.status === "closed")
|
|
185
|
+
return;
|
|
186
|
+
if (attempt < CLOSE_VISIBLE_ATTEMPTS)
|
|
187
|
+
await sleep(CLOSE_VISIBLE_INTERVAL_MS);
|
|
188
|
+
}
|
|
189
|
+
throw new CliError(`the close landed in ${closeTxHash} but the RPC endpoint still reports the stream as active`, {
|
|
190
|
+
exitCode: 4,
|
|
191
|
+
hint: `the close is not lost and neither is the money: re-run \`metrik reclaim ${streamId} --close-first\` once the endpoint has caught up — it will skip the close and reclaim.`,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* `reclaimWithProof` reverts with `SettlementWindowOpen` until the oracle has
|
|
196
|
+
* published a checkpoint whose `checkedAt` is at or after the stream's stop
|
|
197
|
+
* time. That is not an error condition — it is the normal ordering of a
|
|
198
|
+
* checkpoint rail, and it happens on the FIRST attempt essentially every time.
|
|
199
|
+
* A CLI that surfaced the raw revert would teach every user that reclaim is
|
|
200
|
+
* broken, so the wait is built in and narrated. Any other failure is rethrown
|
|
201
|
+
* immediately: only an open settlement window is retried.
|
|
202
|
+
*/
|
|
203
|
+
export async function reclaimWithBoundedRetry(write, buildInput, options) {
|
|
204
|
+
let lastError;
|
|
205
|
+
for (let attempt = 1; attempt <= options.attempts; attempt++) {
|
|
206
|
+
try {
|
|
207
|
+
return await write.reclaimStream(await buildInput());
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
if (!isSettlementWindowOpen(error))
|
|
211
|
+
throw explainReclaimFailure(error);
|
|
212
|
+
lastError = error;
|
|
213
|
+
if (attempt === options.attempts)
|
|
214
|
+
break;
|
|
215
|
+
options.onRetry(attempt, describeError(error));
|
|
216
|
+
await options.sleep(options.intervalMs);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
throw new CliError(`reclaim is still blocked after ${options.attempts} attempt${options.attempts === 1 ? "" : "s"}: the oracle has not published a checkpoint covering this stream's stop time`, {
|
|
220
|
+
exitCode: 4,
|
|
221
|
+
hint: (await options.exhaustedHint?.().catch(() => null)) ??
|
|
222
|
+
DEFAULT_EXHAUSTED_HINT,
|
|
223
|
+
cause: lastError,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
const DEFAULT_EXHAUSTED_HINT = "the funds are not lost — retry later, or raise --attempts / --interval. If the oracle stays down, `metrik reclaim-unverified` takes them back with no proof once the escape hatch opens.";
|
|
227
|
+
/**
|
|
228
|
+
* What to do when the proof-based path has run out of attempts.
|
|
229
|
+
*
|
|
230
|
+
* Every reclaim this command can perform goes through `reclaimWithProof`, which
|
|
231
|
+
* needs a Merkle proof fetched from the oracle — so a buyer whose oracle is gone
|
|
232
|
+
* can retry this command forever and never get their money. The escrow's
|
|
233
|
+
* `reclaimUnverified` is the path that does not need the oracle, and the only
|
|
234
|
+
* question that matters is whether its window has opened yet. That is fully
|
|
235
|
+
* computable: `expiresAt + escapeGraceSeconds` against the chain's clock.
|
|
236
|
+
*/
|
|
237
|
+
async function describeLastResort(read, streamId, fallbackNowSeconds) {
|
|
238
|
+
const [stream, grace] = await Promise.all([
|
|
239
|
+
read.getStreamV2(streamId),
|
|
240
|
+
read.escapeGraceSeconds(),
|
|
241
|
+
]);
|
|
242
|
+
const now = await readChainNow(read, fallbackNowSeconds);
|
|
243
|
+
const window = escapeHatchWindow(stream, grace, now.seconds);
|
|
244
|
+
const escapeCommand = `metrik reclaim-unverified ${streamId} --forfeit-unclaimed`;
|
|
245
|
+
return window.ready
|
|
246
|
+
? `the funds are not lost. The no-proof escape hatch is OPEN (${describeEscapeHatch(window)}): \`${escapeCommand}\` takes them back without the oracle — at the cost of forfeiting whatever the operator earned but has not yet claimed. Retrying this command only helps if the oracle comes back.`
|
|
247
|
+
: `the funds are not lost, but retrying only helps if the oracle comes back — this path needs a proof from it. The no-proof escape hatch ${describeEscapeHatch(window)}; from then on \`${escapeCommand}\` takes them back without the oracle, forfeiting whatever the operator earned but has not yet claimed.`;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* `StreamNotReclaimable()` means exactly one thing: the stream is neither closed
|
|
251
|
+
* nor past its expiry. Say that, and say which command fixes it, instead of
|
|
252
|
+
* handing the reader a selector.
|
|
253
|
+
*/
|
|
254
|
+
function explainReclaimFailure(error) {
|
|
255
|
+
if (!(error instanceof Error))
|
|
256
|
+
return error;
|
|
257
|
+
for (let current = error, depth = 0; depth < 8; depth++) {
|
|
258
|
+
if (!(current instanceof Error))
|
|
259
|
+
break;
|
|
260
|
+
if (current.message.includes("StreamNotReclaimable")) {
|
|
261
|
+
return new CliError("the stream is still running, so there is nothing to reclaim yet", {
|
|
262
|
+
exitCode: 4,
|
|
263
|
+
hint: "stop the clock first — `metrik close <streamId>`, or re-run this command with --close-first — or wait for the stream to expire.",
|
|
264
|
+
cause: error,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
if (current.cause === undefined)
|
|
268
|
+
break;
|
|
269
|
+
current = current.cause;
|
|
270
|
+
}
|
|
271
|
+
return error;
|
|
272
|
+
}
|
|
273
|
+
/** True when `error`, or anything in its cause chain, is SettlementWindowOpen. */
|
|
274
|
+
function isSettlementWindowOpen(error) {
|
|
275
|
+
for (let current = error, depth = 0; depth < 8; depth++) {
|
|
276
|
+
if (current instanceof SettlementWindowOpenError)
|
|
277
|
+
return true;
|
|
278
|
+
if (!(current instanceof Error) || current.cause === undefined)
|
|
279
|
+
return false;
|
|
280
|
+
current = current.cause;
|
|
281
|
+
}
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Bounded poll for the post-reclaim stream state. Returns as soon as the node
|
|
286
|
+
* reports `reclaimed`, or the last successful read once attempts are exhausted
|
|
287
|
+
* (never throws — the reclaim already succeeded and a display read must not
|
|
288
|
+
* turn that into a command failure).
|
|
289
|
+
*/
|
|
290
|
+
const SETTLED_READ_ATTEMPTS = 5;
|
|
291
|
+
// Deliberately sub-second and distinct from the retry interval (whole seconds,
|
|
292
|
+
// `--interval`), so the two waits are tellable apart in tests and in logs.
|
|
293
|
+
const SETTLED_READ_INTERVAL_MS = 500;
|
|
294
|
+
async function readSettledStream(read, streamId, sleep) {
|
|
295
|
+
let last = null;
|
|
296
|
+
for (let attempt = 1; attempt <= SETTLED_READ_ATTEMPTS; attempt += 1) {
|
|
297
|
+
const stream = await read.getStreamV2(streamId).catch(() => null);
|
|
298
|
+
if (stream !== null) {
|
|
299
|
+
last = stream;
|
|
300
|
+
if (stream.reclaimed)
|
|
301
|
+
return stream;
|
|
302
|
+
}
|
|
303
|
+
if (attempt < SETTLED_READ_ATTEMPTS)
|
|
304
|
+
await sleep(SETTLED_READ_INTERVAL_MS);
|
|
305
|
+
}
|
|
306
|
+
return last;
|
|
307
|
+
}
|
|
308
|
+
//# sourceMappingURL=reclaim.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reclaim.js","sourceRoot":"","sources":["../../../src/cli/commands/reclaim.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/status.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAkB,WAAW,EAAE,MAAM,eAAe,CAAC;AAEjE,eAAO,MAAM,aAAa,EAAE,WA6C3B,CAAC"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { attesterDisclosure } from "../attesters.js";
|
|
2
|
+
import { assertTestnetRpc, parseBytes32 } from "../config.js";
|
|
3
|
+
import { usageError } from "../errors.js";
|
|
4
|
+
import { readStreamReport, streamReportJson, streamReportRows, } from "../status.js";
|
|
5
|
+
export const statusCommand = {
|
|
6
|
+
name: "status",
|
|
7
|
+
summary: "Show a stream's on-chain state and its oracle-verified accrual",
|
|
8
|
+
usage: "metrik status <streamId>",
|
|
9
|
+
flags: [],
|
|
10
|
+
signs: false,
|
|
11
|
+
async run(context) {
|
|
12
|
+
const { args, config, out, deps } = context;
|
|
13
|
+
const [streamIdArg] = args.positionals;
|
|
14
|
+
if (streamIdArg === undefined) {
|
|
15
|
+
throw usageError("metrik status requires a <streamId>");
|
|
16
|
+
}
|
|
17
|
+
const streamId = parseBytes32(streamIdArg, "<streamId>");
|
|
18
|
+
const read = deps.createReadClient(config);
|
|
19
|
+
// Reads get the same chain allowlist as the signing paths. A read pointed at
|
|
20
|
+
// the wrong chain does not lose money, but the V1 and V2 stream tuples
|
|
21
|
+
// diverge from index 5 onward, so a wrong-chain read does not revert — it
|
|
22
|
+
// silently decodes something else into fields with the right names.
|
|
23
|
+
await assertTestnetRpc(config, () => read.getChainId());
|
|
24
|
+
const report = await readStreamReport(read, streamId, deps.nowSeconds());
|
|
25
|
+
out.result(streamReportJson(report));
|
|
26
|
+
// Who attests, before the numbers. Printed on stderr so it survives `--json`
|
|
27
|
+
// and cannot be lost by a consumer that only parses stdout.
|
|
28
|
+
const disclosure = attesterDisclosure(report.attesters, report.stream.operator);
|
|
29
|
+
if (disclosure !== null)
|
|
30
|
+
out.note(disclosure);
|
|
31
|
+
out.print(`Stream ${streamId}`);
|
|
32
|
+
out.table(streamReportRows(report));
|
|
33
|
+
if (report.settlement.pending) {
|
|
34
|
+
out.print("");
|
|
35
|
+
out.print("This stream has stopped but its final checkpoint has not landed, so there is no settled reclaimable figure yet — reclaim reverts until the oracle publishes a checkpoint reaching the stop time. `metrik reclaim <streamId>` waits for exactly that.");
|
|
36
|
+
}
|
|
37
|
+
if (report.proof === null) {
|
|
38
|
+
out.print("");
|
|
39
|
+
out.print("No checkpoint covers this stream yet. Accrual is published by the oracle roughly every 15-22s; funds are earned only for intervals that verified.");
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
//# sourceMappingURL=status.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"status.js","sourceRoot":"","sources":["../../../src/cli/commands/status.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC9D,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EACL,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,cAAc,CAAC;AAGtB,MAAM,CAAC,MAAM,aAAa,GAAgB;IACxC,IAAI,EAAE,QAAQ;IACd,OAAO,EAAE,gEAAgE;IACzE,KAAK,EAAE,0BAA0B;IACjC,KAAK,EAAE,EAAE;IACT,KAAK,EAAE,KAAK;IACZ,KAAK,CAAC,GAAG,CAAC,OAAuB;QAC/B,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC;QAC5C,MAAM,CAAC,WAAW,CAAC,GAAG,IAAI,CAAC,WAAW,CAAC;QACvC,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;YAC9B,MAAM,UAAU,CAAC,qCAAqC,CAAC,CAAC;QAC1D,CAAC;QACD,MAAM,QAAQ,GAAG,YAAY,CAAC,WAAW,EAAE,YAAY,CAAC,CAAC;QAEzD,MAAM,IAAI,GAAG,IAAI,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC;QAC3C,6EAA6E;QAC7E,uEAAuE;QACvE,0EAA0E;QAC1E,oEAAoE;QACpE,MAAM,gBAAgB,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC;QACxD,MAAM,MAAM,GAAG,MAAM,gBAAgB,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC;QAEzE,GAAG,CAAC,MAAM,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC,CAAC;QACrC,6EAA6E;QAC7E,4DAA4D;QAC5D,MAAM,UAAU,GAAG,kBAAkB,CACnC,MAAM,CAAC,SAAS,EAChB,MAAM,CAAC,MAAM,CAAC,QAAQ,CACvB,CAAC;QACF,IAAI,UAAU,KAAK,IAAI;YAAE,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QAC9C,GAAG,CAAC,KAAK,CAAC,UAAU,QAAQ,EAAE,CAAC,CAAC;QAChC,GAAG,CAAC,KAAK,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC,CAAC;QACpC,IAAI,MAAM,CAAC,UAAU,CAAC,OAAO,EAAE,CAAC;YAC9B,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;YACd,GAAG,CAAC,KAAK,CACP,sPAAsP,CACvP,CAAC;QACJ,CAAC;QACD,IAAI,MAAM,CAAC,KAAK,KAAK,IAAI,EAAE,CAAC;YAC1B,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;YACd,GAAG,CAAC,KAAK,CACP,mJAAmJ,CACpJ,CAAC;QACJ,CAAC;IACH,CAAC;CACF,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"streams.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/streams.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAkB,WAAW,EAAE,MAAM,eAAe,CAAC;AA8BjE,eAAO,MAAM,cAAc,EAAE,WA6K5B,CAAC"}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { BASE_SEPOLIA_ESCROW_V2 } from "@absol-labs/sdk";
|
|
2
|
+
import { getAddress, isAddress } from "viem";
|
|
3
|
+
import { integerFlag, stringFlag } from "../args.js";
|
|
4
|
+
import { formatUsdc } from "../amounts.js";
|
|
5
|
+
import { BASE_SEPOLIA_ESCROW_V2_DEPLOY_BLOCK, assertTestnetRpc, } from "../config.js";
|
|
6
|
+
import { CliError, configError, usageError } from "../errors.js";
|
|
7
|
+
import { escrowScanRange } from "../scan-range.js";
|
|
8
|
+
import { optionalCliWallet, PRIVATE_KEY_ENV } from "../wallet.js";
|
|
9
|
+
/** Indexed-party filters, and the `--role` values that select them. */
|
|
10
|
+
const ROLES = ["buyer", "operator"];
|
|
11
|
+
/**
|
|
12
|
+
* How many `eth_getLogs` calls are in flight at once. The full history of the
|
|
13
|
+
* live escrow is ~106 windows x 2 role filters; firing those unbounded is how a
|
|
14
|
+
* public RPC starts answering 429, and a rate-limited window is a window this
|
|
15
|
+
* command has to report as unscanned.
|
|
16
|
+
*/
|
|
17
|
+
const SCAN_CONCURRENCY = 6;
|
|
18
|
+
/**
|
|
19
|
+
* Attempts per window before it is reported as unscanned. A public RPC answers
|
|
20
|
+
* a burst of `eth_getLogs` with an occasional 429 — measured against
|
|
21
|
+
* `sepolia.base.org`, 2 of 212 windows on a full-history scan — and a transient
|
|
22
|
+
* rate-limit is NOT a window that cannot be scanned. Declaring it one would
|
|
23
|
+
* make the "this scan is incomplete" warning routine, and a warning that fires
|
|
24
|
+
* on every healthy run is a warning nobody reads on the run that matters.
|
|
25
|
+
*/
|
|
26
|
+
const WINDOW_ATTEMPTS = 3;
|
|
27
|
+
const RETRY_DELAY_MS = 400;
|
|
28
|
+
export const streamsCommand = {
|
|
29
|
+
name: "streams",
|
|
30
|
+
summary: "List the streams a wallet has opened, or been hired through",
|
|
31
|
+
usage: "metrik streams [--address 0x…] [--role buyer|operator|any] [--limit N] [--from-block N]",
|
|
32
|
+
flags: ["address", "role", "limit", "from-block"],
|
|
33
|
+
signs: false,
|
|
34
|
+
async run(context) {
|
|
35
|
+
const { args, config, env, out, deps } = context;
|
|
36
|
+
const roleFlag = stringFlag(args, "role") ?? "any";
|
|
37
|
+
if (roleFlag !== "any" && !ROLES.includes(roleFlag)) {
|
|
38
|
+
throw usageError(`--role must be buyer, operator or any (got "${roleFlag}")`);
|
|
39
|
+
}
|
|
40
|
+
const roles = roleFlag === "any" ? ROLES : [roleFlag];
|
|
41
|
+
const limit = integerFlag(args, "limit");
|
|
42
|
+
if (limit !== undefined && limit < 1) {
|
|
43
|
+
throw usageError("--limit must be at least 1");
|
|
44
|
+
}
|
|
45
|
+
const address = await resolveScanAddress(context);
|
|
46
|
+
const read = deps.createReadClient(config);
|
|
47
|
+
// The same allowlist the signing paths get. This command spends nothing,
|
|
48
|
+
// but it reads a V2 stream tuple, and the V1 and V2 tuples diverge from
|
|
49
|
+
// index 5 onward — a wrong-chain read does not revert, it decodes something
|
|
50
|
+
// else into fields with the right names.
|
|
51
|
+
await assertTestnetRpc(config, () => read.getChainId());
|
|
52
|
+
const anchor = resolveAnchorBlock(args, config.escrow);
|
|
53
|
+
const head = await read.blockNumber();
|
|
54
|
+
const range = escrowScanRange(head, anchor.block);
|
|
55
|
+
// Retention canary, before the scan is believed. A node that has pruned its
|
|
56
|
+
// log index answers a range it no longer retains with a SUCCESSFUL EMPTY
|
|
57
|
+
// result (measured on Base Sepolia: one public endpoint returned 0 logs for
|
|
58
|
+
// a window where two others returned 582). This CLI talks to ONE endpoint,
|
|
59
|
+
// so it cannot recover those logs from a sibling the way a multi-provider
|
|
60
|
+
// client can — but it can refuse to call the silence "no streams".
|
|
61
|
+
const retention = anchor.canary === null
|
|
62
|
+
? null
|
|
63
|
+
: await read
|
|
64
|
+
.escrowLogCountAt(anchor.canary)
|
|
65
|
+
.then((count) => count > 0)
|
|
66
|
+
.catch(() => false);
|
|
67
|
+
const scan = await scanOpenedStreams(read, address, roles, range.windows, deps.sleep);
|
|
68
|
+
const reasons = [];
|
|
69
|
+
if (range.degraded) {
|
|
70
|
+
reasons.push(`the window budget could not reach block ${anchor.block} (scan starts at ${range.fromBlock})`);
|
|
71
|
+
}
|
|
72
|
+
if (scan.failedWindows > 0) {
|
|
73
|
+
reasons.push(`${scan.failedWindows} of ${range.windows.length * roles.length} log queries failed after ${WINDOW_ATTEMPTS} attempts each`);
|
|
74
|
+
}
|
|
75
|
+
if (retention === false) {
|
|
76
|
+
reasons.push(`${config.rpcUrl} does not serve logs for block ${anchor.canary}, so it has pruned the escrow's history and its empty windows cannot be trusted`);
|
|
77
|
+
}
|
|
78
|
+
const degraded = reasons.length > 0;
|
|
79
|
+
// Newest first: the stream someone is looking for is almost always the one
|
|
80
|
+
// they just opened.
|
|
81
|
+
const opened = [...scan.opened].sort((a, b) => a.blockNumber === b.blockNumber
|
|
82
|
+
? 0
|
|
83
|
+
: a.blockNumber > b.blockNumber
|
|
84
|
+
? -1
|
|
85
|
+
: 1);
|
|
86
|
+
const shown = limit === undefined ? opened : opened.slice(0, limit);
|
|
87
|
+
// One `getStream` per row. A list that cannot distinguish an active stream
|
|
88
|
+
// from a closed one is not actionable, and status is the one field the
|
|
89
|
+
// event cannot carry. The oracle's checkpoint proof is deliberately NOT
|
|
90
|
+
// fetched here — that is a per-stream HTTP call, and `metrik status` is the
|
|
91
|
+
// command that makes it.
|
|
92
|
+
const rows = await Promise.all(shown.map(async (log) => {
|
|
93
|
+
const stream = await read.getStreamV2(log.streamId).catch(() => null);
|
|
94
|
+
return {
|
|
95
|
+
streamId: log.streamId,
|
|
96
|
+
role: describeRole(log, address),
|
|
97
|
+
buyer: log.buyer,
|
|
98
|
+
operator: log.operator,
|
|
99
|
+
serviceRef: log.serviceRef,
|
|
100
|
+
// V2 MUTATES `Stream.deposit` on reclaim, so the event is the only
|
|
101
|
+
// surviving record of what was originally escrowed.
|
|
102
|
+
originalDepositUsdc: log.deposit,
|
|
103
|
+
ratePerSecondUsdc: log.ratePerSecond,
|
|
104
|
+
expiresAt: log.expiresAt,
|
|
105
|
+
openedAtBlock: log.blockNumber,
|
|
106
|
+
// A read failure is reported as unread, never as a dropped row: a
|
|
107
|
+
// stream that vanishes from this list is the exact failure the whole
|
|
108
|
+
// command is built to avoid.
|
|
109
|
+
status: stream?.status ?? "unread",
|
|
110
|
+
reclaimed: stream?.reclaimed ?? null,
|
|
111
|
+
};
|
|
112
|
+
}));
|
|
113
|
+
out.result({
|
|
114
|
+
address,
|
|
115
|
+
role: roleFlag,
|
|
116
|
+
escrow: config.escrow,
|
|
117
|
+
fromBlock: range.fromBlock,
|
|
118
|
+
toBlock: range.toBlock,
|
|
119
|
+
windows: range.windows.length,
|
|
120
|
+
logRetentionProven: retention,
|
|
121
|
+
degraded,
|
|
122
|
+
degradedReasons: reasons,
|
|
123
|
+
found: opened.length,
|
|
124
|
+
count: rows.length,
|
|
125
|
+
streams: rows,
|
|
126
|
+
});
|
|
127
|
+
if (degraded) {
|
|
128
|
+
// stderr in both modes: a partial scan must reach someone piping stdout
|
|
129
|
+
// into jq just as loudly as someone reading the table.
|
|
130
|
+
out.note(`warning: this scan is INCOMPLETE — ${reasons.join("; ")}. Streams may exist that are not listed below.`);
|
|
131
|
+
}
|
|
132
|
+
if (rows.length === 0) {
|
|
133
|
+
out.print(degraded
|
|
134
|
+
? `No streams found for ${address} in the part of the escrow's history that could be scanned. This is NOT a statement that none exist — see the warning above.`
|
|
135
|
+
: `No streams for ${address} across the escrow's full history (blocks ${range.fromBlock}-${range.toBlock}).`);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
out.print(`${rows.length} stream${rows.length === 1 ? "" : "s"} for ${address}${opened.length > rows.length ? ` (of ${opened.length}, limited)` : ""}`);
|
|
139
|
+
for (const row of rows) {
|
|
140
|
+
out.print("");
|
|
141
|
+
out.print(`${row.streamId} [${row.status}]`);
|
|
142
|
+
out.table([
|
|
143
|
+
["role", row.role],
|
|
144
|
+
["counterparty", row.role === "buyer" ? row.operator : row.buyer],
|
|
145
|
+
["serviceRef", row.serviceRef],
|
|
146
|
+
["escrowed", formatUsdc(row.originalDepositUsdc)],
|
|
147
|
+
["rate", `${formatUsdc(row.ratePerSecondUsdc)} / second`],
|
|
148
|
+
["opened at block", row.openedAtBlock.toString()],
|
|
149
|
+
[
|
|
150
|
+
"reclaimed",
|
|
151
|
+
row.reclaimed === null ? "unread" : String(row.reclaimed),
|
|
152
|
+
],
|
|
153
|
+
]);
|
|
154
|
+
}
|
|
155
|
+
out.print("");
|
|
156
|
+
out.print("Earned-so-far is published by the oracle, not held on the stream: metrik status <streamId>");
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
/** Whose streams to list: an explicit `--address`, else the configured wallet. */
|
|
160
|
+
async function resolveScanAddress(context) {
|
|
161
|
+
const { args, env, out } = context;
|
|
162
|
+
const explicit = stringFlag(args, "address");
|
|
163
|
+
if (explicit !== undefined) {
|
|
164
|
+
if (!isAddress(explicit)) {
|
|
165
|
+
throw usageError(`--address must be an EVM address (got "${explicit}")`);
|
|
166
|
+
}
|
|
167
|
+
return getAddress(explicit);
|
|
168
|
+
}
|
|
169
|
+
// This command broadcasts nothing and signs nothing, so it must not demand a
|
|
170
|
+
// wallet: `--address` is enough to read anyone's streams. A configured wallet
|
|
171
|
+
// is simply the default subject.
|
|
172
|
+
const wallet = await optionalCliWallet(env, { note: out.note });
|
|
173
|
+
if (wallet === null) {
|
|
174
|
+
throw configError("metrik streams needs a wallet to know whose streams to list", `pass --address 0x… to list any address's streams without one, or set ${PRIVATE_KEY_ENV} (or the CDP or Privy variables).`);
|
|
175
|
+
}
|
|
176
|
+
return wallet.account.address;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Where the log scan starts, and whether a retention canary is possible there.
|
|
180
|
+
*
|
|
181
|
+
* The canary block must be one that is KNOWN to carry escrow logs, otherwise a
|
|
182
|
+
* zero from it proves nothing and every scan would report itself degraded
|
|
183
|
+
* forever. That is only true of the deploy block of the escrow we shipped the
|
|
184
|
+
* constant for — so an overridden `--escrow` or `--from-block` scans without a
|
|
185
|
+
* canary rather than with a meaningless one.
|
|
186
|
+
*/
|
|
187
|
+
function resolveAnchorBlock(args, escrow) {
|
|
188
|
+
const override = integerFlag(args, "from-block");
|
|
189
|
+
const isKnownEscrow = escrow.toLowerCase() === BASE_SEPOLIA_ESCROW_V2.toLowerCase();
|
|
190
|
+
if (override !== undefined) {
|
|
191
|
+
return { block: BigInt(override), canary: null };
|
|
192
|
+
}
|
|
193
|
+
if (!isKnownEscrow) {
|
|
194
|
+
throw configError(`no scan anchor is known for escrow ${escrow}`, "the scan is anchored at the escrow's deploy block, and this CLI only ships that block for its default Base Sepolia escrow. Pass --from-block <deploy block> for a different escrow.");
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
block: BASE_SEPOLIA_ESCROW_V2_DEPLOY_BLOCK,
|
|
198
|
+
canary: BASE_SEPOLIA_ESCROW_V2_DEPLOY_BLOCK,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Page every window for every selected role filter, bounded concurrency.
|
|
203
|
+
*
|
|
204
|
+
* A window that ERRORS is counted, not thrown: a partial list with an honest
|
|
205
|
+
* warning is more useful than no list at all, and the count is what makes the
|
|
206
|
+
* warning specific.
|
|
207
|
+
*/
|
|
208
|
+
async function scanOpenedStreams(read, address, roles, windows, sleep) {
|
|
209
|
+
// A wallet that is BOTH buyer and operator on one stream matches two filters.
|
|
210
|
+
const byStreamId = new Map();
|
|
211
|
+
let failedWindows = 0;
|
|
212
|
+
const readWindow = async (query) => {
|
|
213
|
+
for (let attempt = 1; attempt <= WINDOW_ATTEMPTS; attempt++) {
|
|
214
|
+
try {
|
|
215
|
+
return await read.streamOpenedLogs(query);
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
if (attempt < WINDOW_ATTEMPTS)
|
|
219
|
+
await sleep(RETRY_DELAY_MS * attempt);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return null;
|
|
223
|
+
};
|
|
224
|
+
const queries = windows.flatMap(([fromBlock, toBlock]) => roles.map((role) => ({ role, address, fromBlock, toBlock })));
|
|
225
|
+
for (let index = 0; index < queries.length; index += SCAN_CONCURRENCY) {
|
|
226
|
+
const batch = queries.slice(index, index + SCAN_CONCURRENCY);
|
|
227
|
+
const settled = await Promise.all(batch.map(readWindow));
|
|
228
|
+
for (const logs of settled) {
|
|
229
|
+
if (logs === null) {
|
|
230
|
+
failedWindows++;
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
for (const log of logs) {
|
|
234
|
+
if (!byStreamId.has(log.streamId))
|
|
235
|
+
byStreamId.set(log.streamId, log);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return { opened: [...byStreamId.values()], failedWindows };
|
|
240
|
+
}
|
|
241
|
+
function describeRole(log, address) {
|
|
242
|
+
const wanted = address.toLowerCase();
|
|
243
|
+
const isBuyer = log.buyer.toLowerCase() === wanted;
|
|
244
|
+
const isOperator = log.operator.toLowerCase() === wanted;
|
|
245
|
+
if (isBuyer && isOperator)
|
|
246
|
+
return "buyer+operator";
|
|
247
|
+
if (isBuyer)
|
|
248
|
+
return "buyer";
|
|
249
|
+
if (isOperator)
|
|
250
|
+
return "operator";
|
|
251
|
+
// Only reachable if the node ignored the indexed topic filter it was given.
|
|
252
|
+
throw new CliError(`the RPC returned stream ${log.streamId}, which does not involve ${address}`, {
|
|
253
|
+
exitCode: 4,
|
|
254
|
+
hint: "the escrow log filter was not applied by the endpoint; the result cannot be trusted.",
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
//# sourceMappingURL=streams.js.map
|