@absol-labs/agent 0.9.1 → 0.10.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.
Files changed (199) hide show
  1. package/README.md +77 -3
  2. package/dist/cli/amounts.d.ts +26 -0
  3. package/dist/cli/amounts.d.ts.map +1 -0
  4. package/dist/cli/amounts.js +71 -0
  5. package/dist/cli/amounts.js.map +1 -0
  6. package/dist/cli/args.d.ts +32 -0
  7. package/dist/cli/args.d.ts.map +1 -0
  8. package/dist/cli/args.js +119 -0
  9. package/dist/cli/args.js.map +1 -0
  10. package/dist/cli/attesters.d.ts +51 -0
  11. package/dist/cli/attesters.d.ts.map +1 -0
  12. package/dist/cli/attesters.js +58 -0
  13. package/dist/cli/attesters.js.map +1 -0
  14. package/dist/cli/bin.d.ts +3 -0
  15. package/dist/cli/bin.d.ts.map +1 -0
  16. package/dist/cli/bin.js +5 -0
  17. package/dist/cli/bin.js.map +1 -0
  18. package/dist/cli/commands/claim.d.ts +13 -0
  19. package/dist/cli/commands/claim.d.ts.map +1 -0
  20. package/dist/cli/commands/claim.js +86 -0
  21. package/dist/cli/commands/claim.js.map +1 -0
  22. package/dist/cli/commands/close.d.ts +3 -0
  23. package/dist/cli/commands/close.d.ts.map +1 -0
  24. package/dist/cli/commands/close.js +43 -0
  25. package/dist/cli/commands/close.js.map +1 -0
  26. package/dist/cli/commands/discover.d.ts +3 -0
  27. package/dist/cli/commands/discover.d.ts.map +1 -0
  28. package/dist/cli/commands/discover.js +183 -0
  29. package/dist/cli/commands/discover.js.map +1 -0
  30. package/dist/cli/commands/invoke.d.ts +3 -0
  31. package/dist/cli/commands/invoke.d.ts.map +1 -0
  32. package/dist/cli/commands/invoke.js +141 -0
  33. package/dist/cli/commands/invoke.js.map +1 -0
  34. package/dist/cli/commands/mcp.d.ts +3 -0
  35. package/dist/cli/commands/mcp.d.ts.map +1 -0
  36. package/dist/cli/commands/mcp.js +113 -0
  37. package/dist/cli/commands/mcp.js.map +1 -0
  38. package/dist/cli/commands/open.d.ts +3 -0
  39. package/dist/cli/commands/open.d.ts.map +1 -0
  40. package/dist/cli/commands/open.js +279 -0
  41. package/dist/cli/commands/open.js.map +1 -0
  42. package/dist/cli/commands/reclaim-unverified.d.ts +3 -0
  43. package/dist/cli/commands/reclaim-unverified.d.ts.map +1 -0
  44. package/dist/cli/commands/reclaim-unverified.js +164 -0
  45. package/dist/cli/commands/reclaim-unverified.js.map +1 -0
  46. package/dist/cli/commands/reclaim.d.ts +33 -0
  47. package/dist/cli/commands/reclaim.d.ts.map +1 -0
  48. package/dist/cli/commands/reclaim.js +297 -0
  49. package/dist/cli/commands/reclaim.js.map +1 -0
  50. package/dist/cli/commands/status.d.ts +3 -0
  51. package/dist/cli/commands/status.d.ts.map +1 -0
  52. package/dist/cli/commands/status.js +43 -0
  53. package/dist/cli/commands/status.js.map +1 -0
  54. package/dist/cli/commands/streams.d.ts +3 -0
  55. package/dist/cli/commands/streams.d.ts.map +1 -0
  56. package/dist/cli/commands/streams.js +257 -0
  57. package/dist/cli/commands/streams.js.map +1 -0
  58. package/dist/cli/commands/watch.d.ts +3 -0
  59. package/dist/cli/commands/watch.d.ts.map +1 -0
  60. package/dist/cli/commands/watch.js +55 -0
  61. package/dist/cli/commands/watch.js.map +1 -0
  62. package/dist/cli/config.d.ts +61 -0
  63. package/dist/cli/config.d.ts.map +1 -0
  64. package/dist/cli/config.js +140 -0
  65. package/dist/cli/config.js.map +1 -0
  66. package/dist/cli/context.d.ts +29 -0
  67. package/dist/cli/context.d.ts.map +1 -0
  68. package/dist/cli/context.js +2 -0
  69. package/dist/cli/context.js.map +1 -0
  70. package/dist/cli/deps.d.ts +145 -0
  71. package/dist/cli/deps.d.ts.map +1 -0
  72. package/dist/cli/deps.js +227 -0
  73. package/dist/cli/deps.js.map +1 -0
  74. package/dist/cli/descriptor.d.ts +61 -0
  75. package/dist/cli/descriptor.d.ts.map +1 -0
  76. package/dist/cli/descriptor.js +78 -0
  77. package/dist/cli/descriptor.js.map +1 -0
  78. package/dist/cli/errors.d.ts +48 -0
  79. package/dist/cli/errors.d.ts.map +1 -0
  80. package/dist/cli/errors.js +129 -0
  81. package/dist/cli/errors.js.map +1 -0
  82. package/dist/cli/escape-hatch.d.ts +50 -0
  83. package/dist/cli/escape-hatch.d.ts.map +1 -0
  84. package/dist/cli/escape-hatch.js +45 -0
  85. package/dist/cli/escape-hatch.js.map +1 -0
  86. package/dist/cli/index.d.ts +8 -0
  87. package/dist/cli/index.d.ts.map +1 -0
  88. package/dist/cli/index.js +7 -0
  89. package/dist/cli/index.js.map +1 -0
  90. package/dist/cli/listings.d.ts +15 -0
  91. package/dist/cli/listings.d.ts.map +1 -0
  92. package/dist/cli/listings.js +56 -0
  93. package/dist/cli/listings.js.map +1 -0
  94. package/dist/cli/mandate.d.ts +66 -0
  95. package/dist/cli/mandate.d.ts.map +1 -0
  96. package/dist/cli/mandate.js +61 -0
  97. package/dist/cli/mandate.js.map +1 -0
  98. package/dist/cli/output.d.ts +33 -0
  99. package/dist/cli/output.d.ts.map +1 -0
  100. package/dist/cli/output.js +34 -0
  101. package/dist/cli/output.js.map +1 -0
  102. package/dist/cli/preflight.d.ts +38 -0
  103. package/dist/cli/preflight.d.ts.map +1 -0
  104. package/dist/cli/preflight.js +72 -0
  105. package/dist/cli/preflight.js.map +1 -0
  106. package/dist/cli/progress.d.ts +36 -0
  107. package/dist/cli/progress.d.ts.map +1 -0
  108. package/dist/cli/progress.js +40 -0
  109. package/dist/cli/progress.js.map +1 -0
  110. package/dist/cli/run.d.ts +25 -0
  111. package/dist/cli/run.d.ts.map +1 -0
  112. package/dist/cli/run.js +173 -0
  113. package/dist/cli/run.js.map +1 -0
  114. package/dist/cli/scan-range.d.ts +55 -0
  115. package/dist/cli/scan-range.d.ts.map +1 -0
  116. package/dist/cli/scan-range.js +57 -0
  117. package/dist/cli/scan-range.js.map +1 -0
  118. package/dist/cli/status.d.ts +90 -0
  119. package/dist/cli/status.d.ts.map +1 -0
  120. package/dist/cli/status.js +252 -0
  121. package/dist/cli/status.js.map +1 -0
  122. package/dist/cli/timeout.d.ts +30 -0
  123. package/dist/cli/timeout.d.ts.map +1 -0
  124. package/dist/cli/timeout.js +55 -0
  125. package/dist/cli/timeout.js.map +1 -0
  126. package/dist/cli/version.d.ts +12 -0
  127. package/dist/cli/version.d.ts.map +1 -0
  128. package/dist/cli/version.js +22 -0
  129. package/dist/cli/version.js.map +1 -0
  130. package/dist/cli/wallet.d.ts +95 -0
  131. package/dist/cli/wallet.d.ts.map +1 -0
  132. package/dist/cli/wallet.js +169 -0
  133. package/dist/cli/wallet.js.map +1 -0
  134. package/dist/frameworks/crewai.d.ts +10 -0
  135. package/dist/frameworks/crewai.d.ts.map +1 -1
  136. package/dist/frameworks/crewai.js +12 -2
  137. package/dist/frameworks/crewai.js.map +1 -1
  138. package/dist/gateway/server-entry.d.ts +1 -0
  139. package/dist/gateway/server-entry.js +1 -0
  140. package/dist/gateway/server-entry.js.map +1 -1
  141. package/dist/mcp/http-server.d.ts +1 -0
  142. package/dist/mcp/http-server.js +1 -0
  143. package/dist/mcp/http-server.js.map +1 -1
  144. package/dist/mcp/server.d.ts.map +1 -1
  145. package/dist/mcp/server.js +20 -1
  146. package/dist/mcp/server.js.map +1 -1
  147. package/dist/mcp/stdio.d.ts +1 -0
  148. package/dist/mcp/stdio.js +1 -0
  149. package/dist/mcp/stdio.js.map +1 -1
  150. package/dist/sdk/client.d.ts +25 -1
  151. package/dist/sdk/client.d.ts.map +1 -1
  152. package/dist/sdk/client.js +20 -1
  153. package/dist/sdk/client.js.map +1 -1
  154. package/docs/agent-layer.md +2 -1
  155. package/docs/cli.md +569 -0
  156. package/docs/crewai.md +6 -2
  157. package/docs/eliza.md +6 -1
  158. package/docs/mcp-hosted.md +3 -2
  159. package/docs/quickstart.md +18 -5
  160. package/package.json +17 -5
  161. package/src/cli/amounts.ts +102 -0
  162. package/src/cli/args.ts +150 -0
  163. package/src/cli/attesters.ts +86 -0
  164. package/src/cli/bin.ts +5 -0
  165. package/src/cli/commands/claim.ts +100 -0
  166. package/src/cli/commands/close.ts +53 -0
  167. package/src/cli/commands/discover.ts +228 -0
  168. package/src/cli/commands/invoke.ts +177 -0
  169. package/src/cli/commands/mcp.ts +135 -0
  170. package/src/cli/commands/open.ts +331 -0
  171. package/src/cli/commands/reclaim-unverified.ts +206 -0
  172. package/src/cli/commands/reclaim.ts +360 -0
  173. package/src/cli/commands/status.ts +56 -0
  174. package/src/cli/commands/streams.ts +344 -0
  175. package/src/cli/commands/watch.ts +70 -0
  176. package/src/cli/config.ts +208 -0
  177. package/src/cli/context.ts +30 -0
  178. package/src/cli/deps.ts +437 -0
  179. package/src/cli/descriptor.ts +133 -0
  180. package/src/cli/errors.ts +162 -0
  181. package/src/cli/escape-hatch.ts +92 -0
  182. package/src/cli/index.ts +27 -0
  183. package/src/cli/listings.ts +77 -0
  184. package/src/cli/mandate.ts +126 -0
  185. package/src/cli/output.ts +63 -0
  186. package/src/cli/preflight.ts +88 -0
  187. package/src/cli/progress.ts +75 -0
  188. package/src/cli/run.ts +209 -0
  189. package/src/cli/scan-range.ts +76 -0
  190. package/src/cli/status.ts +381 -0
  191. package/src/cli/timeout.ts +64 -0
  192. package/src/cli/version.ts +21 -0
  193. package/src/cli/wallet.ts +241 -0
  194. package/src/frameworks/crewai.ts +13 -2
  195. package/src/gateway/server-entry.ts +1 -0
  196. package/src/mcp/http-server.ts +1 -0
  197. package/src/mcp/server.ts +25 -1
  198. package/src/mcp/stdio.ts +1 -0
  199. package/src/sdk/client.ts +33 -1
@@ -0,0 +1,75 @@
1
+ /**
2
+ * A heartbeat on stderr for commands that can take a while.
3
+ *
4
+ * A live acceptance run saw `close` take 8s once and 142s the next time, and
5
+ * `status` calls run past two minutes with nothing on screen. With no output at
6
+ * all, a slow RPC and a wedged process look identical, and the only safe reading
7
+ * of that ambiguity — on a command that may have already broadcast a
8
+ * transaction — is the expensive one: wait, or kill it and risk double-sending.
9
+ *
10
+ * So the CLI narrates. Not progress towards a known total (there isn't one), but
11
+ * proof of life plus elapsed time, which is what distinguishes "slow" from
12
+ * "stuck". stderr in BOTH modes, so `--json` stdout stays a single parseable
13
+ * document.
14
+ */
15
+ export interface Heartbeat {
16
+ /** Stops the ticker. Safe to call more than once. */
17
+ readonly stop: () => void;
18
+ }
19
+
20
+ /** Silence before the first tick — below this, a command is simply fast. */
21
+ export const HEARTBEAT_DELAY_MS = 8_000;
22
+ /** Gap between ticks once it has started. */
23
+ export const HEARTBEAT_INTERVAL_MS = 10_000;
24
+
25
+ export interface HeartbeatOptions {
26
+ readonly delayMs?: number;
27
+ readonly intervalMs?: number;
28
+ /** Injectable for tests; defaults to the global timers. */
29
+ readonly setTimer?: typeof setTimeout;
30
+ readonly clearTimer?: typeof clearTimeout;
31
+ readonly now?: () => number;
32
+ }
33
+
34
+ /**
35
+ * Start narrating `label`. Nothing is printed unless the work outlasts
36
+ * `delayMs`, so a fast command is silent and a slow one is legible.
37
+ */
38
+ export function startHeartbeat(
39
+ note: (line: string) => void,
40
+ label: string,
41
+ options: HeartbeatOptions = {},
42
+ ): Heartbeat {
43
+ const delayMs = options.delayMs ?? HEARTBEAT_DELAY_MS;
44
+ const intervalMs = options.intervalMs ?? HEARTBEAT_INTERVAL_MS;
45
+ const setTimer = options.setTimer ?? setTimeout;
46
+ const clearTimer = options.clearTimer ?? clearTimeout;
47
+ const now = options.now ?? Date.now;
48
+
49
+ const startedAt = now();
50
+ let timer: ReturnType<typeof setTimeout> | undefined;
51
+ let stopped = false;
52
+
53
+ const tick = (): void => {
54
+ if (stopped) return;
55
+ const elapsed = Math.round((now() - startedAt) / 1000);
56
+ note(
57
+ `… still working: ${label} (${elapsed}s elapsed). Slow RPC or oracle, not a hang — nothing is retried silently.`,
58
+ );
59
+ schedule(intervalMs);
60
+ };
61
+ const schedule = (ms: number): void => {
62
+ timer = setTimer(tick, ms);
63
+ // Never hold the process open on account of the narration.
64
+ (timer as { unref?: () => void }).unref?.();
65
+ };
66
+ schedule(delayMs);
67
+
68
+ return {
69
+ stop() {
70
+ stopped = true;
71
+ if (timer !== undefined) clearTimer(timer);
72
+ timer = undefined;
73
+ },
74
+ };
75
+ }
package/src/cli/run.ts ADDED
@@ -0,0 +1,209 @@
1
+ import { assertKnownFlags, booleanFlag, parseArgs } from "./args.js";
2
+ import {
3
+ CONFIG_FLAGS,
4
+ GLOBAL_FLAGS,
5
+ TESTNET_CHAIN_IDS,
6
+ resolveConfig,
7
+ } from "./config.js";
8
+ import { CliError, describeError, usageError } from "./errors.js";
9
+ import { createPrinter, type CliStreams } from "./output.js";
10
+ import { startHeartbeat } from "./progress.js";
11
+ import { defaultCliDependencies, type CliDependencies } from "./deps.js";
12
+ import { packageVersion } from "./version.js";
13
+ import { CDP_ENV_VARS, PRIVATE_KEY_ENV } from "./wallet.js";
14
+ import type { CommandSpec } from "./context.js";
15
+
16
+ import { claimCommand } from "./commands/claim.js";
17
+ import { closeCommand } from "./commands/close.js";
18
+ import { discoverCommand } from "./commands/discover.js";
19
+ import { invokeCommand } from "./commands/invoke.js";
20
+ import { mcpCommand } from "./commands/mcp.js";
21
+ import { openCommand } from "./commands/open.js";
22
+ import { reclaimCommand } from "./commands/reclaim.js";
23
+ import { reclaimUnverifiedCommand } from "./commands/reclaim-unverified.js";
24
+ import { statusCommand } from "./commands/status.js";
25
+ import { streamsCommand } from "./commands/streams.js";
26
+ import { watchCommand } from "./commands/watch.js";
27
+
28
+ /**
29
+ * The command surface, in the order the product loop runs:
30
+ * discover -> open -> invoke -> watch/status -> close -> reclaim, then the
31
+ * operator's own redemption (`claim`) and the buyer's last-resort exit
32
+ * (`reclaim-unverified`). `streams` (which of them are mine?) sits inside that
33
+ * loop; `mcp` (run the same loop from an MCP host) sits after it.
34
+ */
35
+ export const COMMANDS: readonly CommandSpec[] = [
36
+ discoverCommand,
37
+ openCommand,
38
+ statusCommand,
39
+ streamsCommand,
40
+ watchCommand,
41
+ invokeCommand,
42
+ closeCommand,
43
+ reclaimCommand,
44
+ reclaimUnverifiedCommand,
45
+ claimCommand,
46
+ mcpCommand,
47
+ ];
48
+
49
+ export interface RunCliOptions {
50
+ readonly argv: readonly string[];
51
+ readonly env?: NodeJS.ProcessEnv;
52
+ readonly streams?: CliStreams;
53
+ readonly deps?: CliDependencies;
54
+ }
55
+
56
+ /**
57
+ * Runs one CLI invocation and returns the process exit code. Pure in its
58
+ * inputs: argv, env, output sinks and side-effecting dependencies are all
59
+ * injected, so the whole surface is testable without a key or an RPC.
60
+ */
61
+ export async function runCli(options: RunCliOptions): Promise<number> {
62
+ const env = options.env ?? process.env;
63
+ const streams = options.streams ?? {
64
+ stdout: (line) => process.stdout.write(`${line}\n`),
65
+ stderr: (line) => process.stderr.write(`${line}\n`),
66
+ };
67
+ const deps = options.deps ?? defaultCliDependencies;
68
+
69
+ let json = false;
70
+ try {
71
+ const args = parseArgs(options.argv);
72
+ json = booleanFlag(args, "json");
73
+ const out = createPrinter(streams, json);
74
+
75
+ // `--version` is checked before the bare-invocation help branch: `metrik
76
+ // --version` parses to no command at all.
77
+ if (args.command === "version" || booleanFlag(args, "version")) {
78
+ streams.stdout(packageVersion());
79
+ return 0;
80
+ }
81
+ if (args.command === null || args.command === "help") {
82
+ // A misspelled TOP-LEVEL flag must fail like a misspelled per-command one
83
+ // does. Printing help and exiting 0 tells a script the invocation
84
+ // succeeded, which is how a typo goes unnoticed.
85
+ assertKnownFlags(args, [...GLOBAL_FLAGS, "version"]);
86
+ streams.stdout(renderHelp(args.positionals[0]));
87
+ return 0;
88
+ }
89
+
90
+ const command = COMMANDS.find((spec) => spec.name === args.command);
91
+ if (command === undefined) {
92
+ throw usageError(
93
+ `unknown command: ${args.command}`,
94
+ `commands: ${COMMANDS.map((spec) => spec.name).join(", ")}`,
95
+ );
96
+ }
97
+ if (booleanFlag(args, "help")) {
98
+ streams.stdout(renderHelp(command.name));
99
+ return 0;
100
+ }
101
+
102
+ assertKnownFlags(args, [
103
+ ...GLOBAL_FLAGS,
104
+ ...CONFIG_FLAGS,
105
+ ...command.flags,
106
+ ]);
107
+ const config = resolveConfig(args, env);
108
+ // Every command gets the heartbeat, in one place rather than per command:
109
+ // any of them can be the one waiting on a slow endpoint, and a rule with an
110
+ // exception is a rule someone forgets to apply to the next command.
111
+ const heartbeat = startHeartbeat(out.note, `metrik ${command.name}`);
112
+ try {
113
+ await command.run({ args, config, env, out, deps });
114
+ } finally {
115
+ heartbeat.stop();
116
+ }
117
+ return 0;
118
+ } catch (error) {
119
+ if (error instanceof CliError) {
120
+ emitFailure(streams, json, error.message, error.hint, error.exitCode);
121
+ return error.exitCode;
122
+ }
123
+ emitFailure(streams, json, describeError(error), undefined, 4);
124
+ return 4;
125
+ }
126
+ }
127
+
128
+ function emitFailure(
129
+ streams: CliStreams,
130
+ json: boolean,
131
+ message: string,
132
+ hint: string | undefined,
133
+ exitCode: number,
134
+ ): void {
135
+ if (json) {
136
+ // The failure goes to stderr even in --json mode so stdout stays either a
137
+ // result document or nothing at all.
138
+ streams.stderr(
139
+ JSON.stringify({ error: message, hint: hint ?? null, exitCode }),
140
+ );
141
+ return;
142
+ }
143
+ streams.stderr(`error: ${message}`);
144
+ if (hint !== undefined) streams.stderr(` ${hint}`);
145
+ }
146
+
147
+ export function renderHelp(commandName?: string): string {
148
+ const command = COMMANDS.find((spec) => spec.name === commandName);
149
+ if (command !== undefined) {
150
+ return [
151
+ `metrik ${command.name} — ${command.summary}`,
152
+ "",
153
+ `Usage: ${command.usage}`,
154
+ "",
155
+ command.flags.length === 0
156
+ ? "Command flags: (none)"
157
+ : `Command flags: ${command.flags.map((flag) => `--${flag}`).join(" ")}`,
158
+ `Settlement flags: ${CONFIG_FLAGS.map((flag) => `--${flag}`).join(" ")}`,
159
+ "Output flags: --json",
160
+ ...(command.signs
161
+ ? [
162
+ "",
163
+ `Broadcasts a transaction. Requires a wallet in the environment: ${PRIVATE_KEY_ENV}, or ${CDP_ENV_VARS.join(" + ")}.`,
164
+ ]
165
+ : command.requiresKey === true
166
+ ? [
167
+ "",
168
+ `Broadcasts nothing, but signs an off-chain EIP-712 capability, so it requires a wallet in the environment: ${PRIVATE_KEY_ENV}, or ${CDP_ENV_VARS.join(" + ")}.`,
169
+ ]
170
+ : []),
171
+ ].join("\n");
172
+ }
173
+
174
+ const width = COMMANDS.reduce(
175
+ (max, spec) => Math.max(max, spec.name.length),
176
+ 0,
177
+ );
178
+ return [
179
+ `metrik ${packageVersion()} — pay for verified delivery, metered by the second`,
180
+ "",
181
+ "You rent a verified-live endpoint BY TIME, not per call: the oracle probes it and the",
182
+ "meter runs on availability that verified. One request and a thousand cost the same",
183
+ "second, and the clock keeps running while you send none. `metrik close` stops it.",
184
+ "",
185
+ "Usage: metrik <command> [options]",
186
+ "",
187
+ "Commands:",
188
+ ...COMMANDS.map(
189
+ (spec) =>
190
+ ` ${spec.name.padEnd(width)} ${spec.summary}${spec.signs ? " [broadcasts a tx]" : spec.requiresKey === true ? " [needs a key]" : ""}`,
191
+ ),
192
+ "",
193
+ "Global flags:",
194
+ " --json machine-readable output on stdout",
195
+ " --help this text, or `metrik <command> --help`",
196
+ "",
197
+ "Settlement flags (all default to Metrik's Base Sepolia deployment):",
198
+ " --chain-id --rpc-url --escrow --usdc --oracle-url --registry-url",
199
+ "",
200
+ "Wallet (read from the environment only; never accepted as a flag):",
201
+ ` ${PRIVATE_KEY_ENV} a Base Sepolia test key. Checked first.`,
202
+ ` ${CDP_ENV_VARS.join(", ")}`,
203
+ " a Coinbase CDP Server Wallet v2 account. Used when no private key is set.",
204
+ " A credential on the command line lands in shell history; these never are.",
205
+ "",
206
+ `TESTNET ONLY: this CLI refuses any chain outside ${TESTNET_CHAIN_IDS.join(", ")} (Base Sepolia),`,
207
+ "and re-checks the chain id the RPC actually serves before signing. There is no override.",
208
+ ].join("\n");
209
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Pure block-range arithmetic for the escrow log scan behind `metrik streams`.
3
+ * No RPC, no clock, no env — so the coverage rules below are unit-testable
4
+ * without a network, which is the point: they decide when an empty result is
5
+ * allowed to be rendered as "you have no streams".
6
+ *
7
+ * ## Why the anchor is the deploy block
8
+ *
9
+ * A fixed lookback ("the last N blocks") cannot describe coverage: a wallet
10
+ * whose stream predates the window is shown an empty list with nothing to
11
+ * distinguish it from a wallet that never opened one. For a payments tool,
12
+ * telling someone their escrowed funds do not exist is the worst failure we
13
+ * have. The escrow has NO history before its deploy block, so that block is the
14
+ * honest anchor — and whether the budget reached it is then a fact the command
15
+ * can state.
16
+ *
17
+ * ## Why the window is 10 000 blocks
18
+ *
19
+ * `buyer` and `operator` are INDEXED on `StreamOpened`, so a per-wallet scan is
20
+ * a server-side topic filter whose response is tiny regardless of range. What
21
+ * public RPCs cap is the per-call BLOCK RANGE, and a topic filter does not lift
22
+ * it: `sepolia.base.org` answers a wider range with `-32614 "eth_getLogs is
23
+ * limited to a 10,000 range"`. So the page size is the cap, not a guess below
24
+ * it.
25
+ */
26
+
27
+ /**
28
+ * getLogs page size. The window is `chunk + 1` blocks (bounds inclusive), so
29
+ * 9 999 is the largest page `sepolia.base.org` accepts.
30
+ */
31
+ export const LOG_SCAN_CHUNK = 9_999n;
32
+
33
+ /**
34
+ * Window budget for one scan. 400 x 10 000 = 4 000 000 blocks, comfortably more
35
+ * than the live escrow's history (~1.06M blocks on 2026-09-17), so the default
36
+ * scan is complete rather than merely large. It is a CEILING, not a fixed cost:
37
+ * windows are built for the actual anchor-to-head span. Exceeding it is not an
38
+ * error — it is reported as `degraded`.
39
+ */
40
+ export const LOG_SCAN_MAX_WINDOWS = 400;
41
+
42
+ export interface ScanRange {
43
+ readonly fromBlock: bigint;
44
+ readonly toBlock: bigint;
45
+ /** Inclusive `[from, to]` windows, in ascending order. */
46
+ readonly windows: readonly (readonly [bigint, bigint])[];
47
+ /** True iff the budget could NOT reach `anchor`, i.e. the scan is partial. */
48
+ readonly degraded: boolean;
49
+ }
50
+
51
+ /**
52
+ * Page `[anchor, head]` into non-overlapping inclusive windows.
53
+ *
54
+ * An RPC that is behind the deployment yields an empty-but-valid range rather
55
+ * than a negative one, and an anchor past the head is clamped: a node still
56
+ * syncing must not produce a nonsense scan.
57
+ */
58
+ export function escrowScanRange(
59
+ head: bigint,
60
+ anchorBlock: bigint,
61
+ chunk: bigint = LOG_SCAN_CHUNK,
62
+ maxWindows: number = LOG_SCAN_MAX_WINDOWS,
63
+ ): ScanRange {
64
+ const anchor =
65
+ anchorBlock < 0n ? 0n : anchorBlock > head ? head : anchorBlock;
66
+ const maxSpan = (chunk + 1n) * BigInt(maxWindows);
67
+ const degraded = head - anchor > maxSpan;
68
+ const fromBlock = degraded ? head - maxSpan : anchor;
69
+
70
+ const windows: (readonly [bigint, bigint])[] = [];
71
+ for (let start = fromBlock; start <= head; start += chunk + 1n) {
72
+ windows.push([start, start + chunk > head ? head : start + chunk]);
73
+ }
74
+
75
+ return { fromBlock, toBlock: head, windows, degraded };
76
+ }