@johannc-dev/zyrk 0.2.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -53,6 +53,13 @@ npx zyrk run wallet countBuys '{"wallet":"…","sinceMs":0,"untilMs":3600000}'
53
53
  autres API réseau dans votre code, et l'ancienne API 0.1 (`ctx.data.query`,
54
54
  `ctx.data.pages`) avec un guide de migration.
55
55
 
56
+ Supprimer un programme : `npx zyrk delete wallet` (demande confirmation ;
57
+ `--yes` dans un script) ou `client.delete("wallet")`. Partent ses versions,
58
+ son état, ses abonnements, ses exécutions (celles en cours sont annulées) et
59
+ ses replays ; `ctx.storage` reste (il appartient au compte). La place est
60
+ libérée tout de suite et le même nom redéployé repart de zéro. Un programme
61
+ absent répond `404 program_not_found`.
62
+
56
63
  Modules :
57
64
 
58
65
  | Import | Contenu |
package/dist/cli.d.ts CHANGED
@@ -1,4 +1,9 @@
1
1
  #!/usr/bin/env node
2
- export declare const VERSION = "0.2.1";
2
+ export declare const VERSION = "0.2.2";
3
3
  /** `2026-11-07`, `2026-11-07T13:00:00Z` or milliseconds since the epoch. */
4
4
  export declare function timeOf(text: string): number;
5
+ /**
6
+ * Whether `zyrk delete` may go on: `--yes`, or a `y` typed at the prompt.
7
+ * Without a terminal (a script, CI) and without `--yes` it refuses.
8
+ */
9
+ export declare function confirmDeletion(name: string, yes: boolean, interactive?: boolean): Promise<boolean>;
package/dist/cli.js CHANGED
@@ -5,10 +5,10 @@ import { ChildProcessSpawner } from "effect/process";
5
5
  import { BUNDLE_PATH, writeBundleEffect } from "./build.js";
6
6
  import { ZyrkClient } from "./client.js";
7
7
  import { decode, Json } from "./contracts.js";
8
- import { exportHistory, exportTotals, exportVolumeSearch, } from "./export.js";
8
+ import { exportHistory, exportTotals, exportVolumeSearch, exportCompute, } from "./export.js";
9
9
  import { LocalRunner } from "./local.js";
10
10
  import { DecodeError, messageOf, } from "./errors.js";
11
- export const VERSION = "0.2.1";
11
+ export const VERSION = "0.2.2";
12
12
  const print = (value) => Console.log(JSON.stringify(value, null, 2));
13
13
  const entry = Argument.String("entry.ts").pipe(Argument.withDescription("TypeScript or JavaScript entry point of the program"));
14
14
  const program = Argument.String("program").pipe(Argument.withDescription("Program name"));
@@ -47,6 +47,26 @@ export function timeOf(text) {
47
47
  throw new Error(`Not a date: ${text}`);
48
48
  return ms;
49
49
  }
50
+ export async function confirmDeletion(name, yes, interactive = process.stdin.isTTY === true) {
51
+ if (yes)
52
+ return true;
53
+ if (!interactive) {
54
+ process.stderr.write(`error[NotConfirmed]: zyrk delete ${name} deletes the program, its state, subscriptions, executions and replays: add --yes to confirm without a terminal\n`);
55
+ return false;
56
+ }
57
+ const { createInterface } = await import("node:readline/promises");
58
+ const prompt = createInterface({
59
+ input: process.stdin,
60
+ output: process.stderr,
61
+ });
62
+ try {
63
+ const answer = await prompt.question(`Delete program ${name} with its state, subscriptions, executions and replays (ctx.storage stays)? [y/N] `);
64
+ return /^y(es)?$/i.test(answer.trim());
65
+ }
66
+ finally {
67
+ prompt.close();
68
+ }
69
+ }
50
70
  const local = (name, body) => Effect.promise(() => body().catch((error) => {
51
71
  process.stderr.write(`error[${name}]: ${error instanceof Error ? error.message : String(error)}\n`);
52
72
  process.exitCode = 1;
@@ -67,9 +87,10 @@ const exportCommand = Command.make("export", {
67
87
  "with-tx": flag("with-tx", "Export batch.tx (transaction of each trade, place in it)"),
68
88
  refresh: flag("refresh", "Download every hour again (a finished export only re-downloads the hours that were open or not sealed, or exported by an older version)"),
69
89
  totals: optionalString("totals", "Also save the totals of this mint (ctx.data.tokenTotals, tokenInfo)"),
70
- quote: optionalString("quote", "Quote mint of --totals"),
90
+ quote: optionalString("quote", "Quote mint of --totals, or auto"),
91
+ compute: optionalString("compute", "Also save the answer of ctx.compute(<json plan or array of plans>), computed by the service"),
71
92
  "volume-search": optionalString("volume-search", "Also save every page of ctx.data.volumeSearch(<json params>)"),
72
- rpm: Flag.Int("rpm").pipe(Flag.withDescription("At most this many requests per minute"), Flag.optional),
93
+ rpm: Flag.Int("rpm").pipe(Flag.withDescription("At most this many requests per minute (optional: the server allows 30 a second per account)"), Flag.optional),
73
94
  out: Flag.String("out").pipe(Flag.withAlias("o"), Flag.withDescription("Data directory (created, resumed if it exists)"), Flag.withDefault("data")),
74
95
  }, (input) => local("export", async () => {
75
96
  const url = process.env.TH_API_URL;
@@ -119,6 +140,12 @@ const exportCommand = Command.make("export", {
119
140
  await exportTotals(connection, input.out, totals, value(input.quote) ?? null);
120
141
  work = true;
121
142
  }
143
+ const computeText = value(input.compute);
144
+ if (computeText) {
145
+ const r = await exportCompute(connection, input.out, JSON.parse(computeText));
146
+ process.stderr.write(`compute: ${r.rows === null ? "an error answer (saved: the local run throws it)" : `${r.rows} row(s)`}\n`);
147
+ work = true;
148
+ }
122
149
  const volume = value(input["volume-search"]);
123
150
  if (volume) {
124
151
  const pages = await exportVolumeSearch(connection, input.out, JSON.parse(volume));
@@ -126,7 +153,7 @@ const exportCommand = Command.make("export", {
126
153
  work = true;
127
154
  }
128
155
  if (!work)
129
- throw new Error("Nothing to export: --token/--wallet, --totals or --volume-search");
156
+ throw new Error("Nothing to export: --token/--wallet, --totals, --volume-search or --compute");
130
157
  })).pipe(Command.withDescription("Download history (one file per hour, as hosted programs see it) for zyrk dev. Resumes an interrupted export."));
131
158
  const devCommand = Command.make("dev", {
132
159
  entry,
@@ -248,6 +275,15 @@ const zyrk = Command.make("zyrk").pipe(Command.withDescription("Zyrk hosted func
248
275
  Command.make("programs", {}, () => withClient((c) => c.programs())).pipe(Command.withDescription("List the programs of the account")),
249
276
  onArgument("inspect", "Show a program", program, (c, p) => c.inspect(p)),
250
277
  onArgument("executions", "Recent executions of a program", program, (c, p) => c.executions(p)),
278
+ Command.make("delete", {
279
+ program,
280
+ yes: flag("yes", "Do not ask for confirmation"),
281
+ }, (input) => Effect.promise(() => confirmDeletion(input.program, input.yes)).pipe(Effect.flatMap((confirmed) => {
282
+ if (confirmed)
283
+ return withClient((c) => c.delete(input.program));
284
+ process.exitCode = 1;
285
+ return Effect.void;
286
+ }))).pipe(Command.withDescription("Delete a program and everything bound to it (state, subscriptions, executions, replays); ctx.storage stays. Asks first unless --yes")),
251
287
  onArgument("pause", "Pause a program", program, (c, p) => c.pause(p, true)),
252
288
  onArgument("resume", "Resume a program", program, (c, p) => c.pause(p, false)),
253
289
  exportCommand,
package/dist/client.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { Context, Effect, Layer } from "effect";
2
- import { Job, Json, ProgramSummary, type JobResult } from "./contracts.js";
2
+ import { Job, Json, ProgramDeleted, ProgramSummary, type JobResult } from "./contracts.js";
3
3
  import { ApiError, ConfigError, DecodeError, ExecutionEnded, ExecutionFailed, NetworkError, WaitTimeout } from "./errors.js";
4
4
  export * from "./errors.js";
5
- export type { Job, JobResult, ProgramSummary } from "./contracts.js";
5
+ export type { Job, JobResult, ProgramDeleted, ProgramSummary, } from "./contracts.js";
6
6
  import type { ReplayFeedItem, ReplayRecord, ReplayRequest } from "./sdk.js";
7
7
  export type { ReplayFeedItem, ReplayRecord, ReplayRequest } from "./sdk.js";
8
8
  export interface ClientConfig {
@@ -46,6 +46,13 @@ export interface ZyrkClientService {
46
46
  programs(): Effect.Effect<readonly ProgramSummary[], RequestError>;
47
47
  inspect(program: string): Effect.Effect<Json, RequestError>;
48
48
  pause(program: string, paused?: boolean): Effect.Effect<Json, RequestError>;
49
+ /**
50
+ * Deletes the program and everything bound to it (versions, state,
51
+ * subscriptions, executions, replays); what runs of it is cancelled.
52
+ * The account's storage stays. Fails with `program_not_found` (404) when
53
+ * there is no such program, also when a retried call already deleted it.
54
+ */
55
+ delete(program: string): Effect.Effect<ProgramDeleted, RequestError>;
49
56
  /** Recent executions of a program (summaries). */
50
57
  executions(program: string): Effect.Effect<Json, RequestError>;
51
58
  /** Starts an execution and returns its id. */
@@ -125,6 +132,8 @@ export declare class FunctionsClient {
125
132
  programs(signal?: AbortSignal): Promise<readonly ProgramSummary[]>;
126
133
  inspect(program: string, signal?: AbortSignal): Promise<Json>;
127
134
  pause(program: string, paused?: boolean, signal?: AbortSignal): Promise<Json>;
135
+ /** See {@link ZyrkClientService.delete}. */
136
+ delete(program: string, signal?: AbortSignal): Promise<ProgramDeleted>;
128
137
  executions(program: string, signal?: AbortSignal): Promise<Json>;
129
138
  start(program: string, input: StartInput, signal?: AbortSignal): Promise<string>;
130
139
  execution(id: string, signal?: AbortSignal): Promise<Job>;
package/dist/client.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Cause, Config, Context, Effect, Exit, Layer, Option, Redacted, Result, Schema, } from "effect";
2
- import { decode, Job, Json, ProgramSummary, Started, toJobResult, } from "./contracts.js";
2
+ import { decode, Job, Json, ProgramDeleted, ProgramSummary, Started, toJobResult, } from "./contracts.js";
3
3
  import { ApiError, ConfigError, DecodeError, ExecutionEnded, ExecutionFailed, Failure, messageOf, NetworkError, WaitTimeout, } from "./errors.js";
4
4
  export * from "./errors.js";
5
5
  const ServerError = Schema.Struct({
@@ -630,6 +630,7 @@ export const make = (config) => {
630
630
  programs: () => request("GET", "programs").pipe(Effect.flatMap(decode(Schema.Array(ProgramSummary), "programs"))),
631
631
  inspect: (program) => request("GET", `programs/${segment(program)}`),
632
632
  pause: (program, paused = true) => request("POST", `programs/${segment(program)}/pause`, { paused }),
633
+ delete: (program) => request("DELETE", `programs/${segment(program)}`).pipe(Effect.flatMap(decode(ProgramDeleted, "delete response"))),
633
634
  executions: (program) => request("GET", `programs/${segment(program)}/executions`),
634
635
  start,
635
636
  execution,
@@ -695,6 +696,9 @@ export class FunctionsClient {
695
696
  pause(program, paused = true, signal) {
696
697
  return runPromise(this.client.pause(program, paused), signal);
697
698
  }
699
+ delete(program, signal) {
700
+ return runPromise(this.client.delete(program), signal);
701
+ }
698
702
  executions(program, signal) {
699
703
  return runPromise(this.client.executions(program), signal);
700
704
  }
@@ -277,6 +277,22 @@ export declare const ProgramSummary: Schema.Struct<{
277
277
  readonly paused: Schema.Boolean;
278
278
  }>;
279
279
  export type ProgramSummary = typeof ProgramSummary.Type;
280
+ /** What `DELETE programs/{program}` removed (counts). */
281
+ export declare const ProgramDeleted: Schema.Struct<{
282
+ readonly deleted: Schema.Boolean;
283
+ readonly program: Schema.String;
284
+ readonly removed: Schema.Struct<{
285
+ readonly versions: Schema.Number;
286
+ readonly stateKeys: Schema.Number;
287
+ readonly subscriptions: Schema.Number;
288
+ readonly executions: Schema.Number;
289
+ readonly cancelledExecutions: Schema.Number;
290
+ readonly replays: Schema.Number;
291
+ readonly cancelledReplays: Schema.Number;
292
+ readonly streamEvents: Schema.Number;
293
+ }>;
294
+ }>;
295
+ export type ProgramDeleted = typeof ProgramDeleted.Type;
280
296
  /** A schema that decodes without Effect services, so decoding is synchronous. */
281
297
  export type Decodable = Schema.Top & {
282
298
  readonly DecodingServices: never;
package/dist/contracts.js CHANGED
@@ -111,6 +111,20 @@ export const ProgramSummary = Schema.Struct({
111
111
  stateVersion: Schema.Number,
112
112
  paused: Schema.Boolean,
113
113
  });
114
+ export const ProgramDeleted = Schema.Struct({
115
+ deleted: Schema.Boolean,
116
+ program: Schema.String,
117
+ removed: Schema.Struct({
118
+ versions: Schema.Number,
119
+ stateKeys: Schema.Number,
120
+ subscriptions: Schema.Number,
121
+ executions: Schema.Number,
122
+ cancelledExecutions: Schema.Number,
123
+ replays: Schema.Number,
124
+ cancelledReplays: Schema.Number,
125
+ streamEvents: Schema.Number,
126
+ }),
127
+ });
114
128
  export const NonNegativeInt = Schema.Number.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(0));
115
129
  export const decodeWith = (schema, options, onError) => {
116
130
  const parse = Schema.decodeUnknownResult(schema);
package/dist/export.d.ts CHANGED
@@ -5,11 +5,11 @@ export interface ExportConnection {
5
5
  readonly url: string;
6
6
  /** API key (`TH_API_KEY`) of an account with hosted functions. */
7
7
  readonly apiKey: string;
8
- /** At most this many requests per minute (default: as fast as the server allows). */
8
+ /** At most this many requests per minute (default: as fast as the server allows, 30 a second per account). Optional pacing only. */
9
9
  readonly rpm?: number;
10
10
  /** Network failures, 502, 503 and 504 tolerated in a row before giving up (default 8); another 5xx is retried once. */
11
11
  readonly maxRetries?: number;
12
- /** Longest wait accepted on 429s in a row before stopping (default 5 minutes): a daily quota stops the export. */
12
+ /** Longest wait accepted on 429s in a row before stopping (default 5 minutes): a longer one (an older server's daily quota) stops the export. */
13
13
  readonly maxWaitMs?: number;
14
14
  readonly fetch?: typeof fetch;
15
15
  readonly signal?: AbortSignal;
@@ -85,6 +85,16 @@ export declare function exportHistory(connection: ExportConnection, out: string,
85
85
  * or the error answer saved for them (`{ error: { kind, code, message } }`).
86
86
  */
87
87
  export declare function exportTotals(connection: ExportConnection, out: string, mint: string, quote?: string | null): Promise<Json>;
88
+ /**
89
+ * Saves the answer of `ctx.compute(plan)` (a plan or an array of plans), as
90
+ * the service computes it for a function (`POST /v1/functions/compute`, the
91
+ * same engine), so that the same call in a local run finds it. Relative
92
+ * windows (`last`, an `until` left out) are resolved now, at export. An
93
+ * error answer (an invalid plan, a limit) is saved as that error.
94
+ */
95
+ export declare function exportCompute(connection: ExportConnection, out: string, plan: Json): Promise<{
96
+ rows: number | null;
97
+ }>;
88
98
  /**
89
99
  * Saves every page of a volume search for `params`, following the
90
100
  * cursor exactly as `ctx.data.volumeSearch` does, so that the same call in
package/dist/export.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
3
  import { decompressHour } from "./hourfile.js";
4
- import { HOUR_MS, READ_FORMAT, infoName, sha256, readId, spec as readSpec, totalsName, volumeName, } from "./local-data.js";
4
+ import { HOUR_MS, READ_FORMAT, infoName, sha256, readId, spec as readSpec, totalsName, volumeName, computeName, } from "./local-data.js";
5
5
  import { MAX_WINDOW_MS } from "./limits.js";
6
6
  const DAY_MS = 86_400_000;
7
7
  const PART_MS = 30 * DAY_MS;
@@ -49,7 +49,7 @@ class Http {
49
49
  constructor(c) {
50
50
  this.c = c;
51
51
  }
52
- async raw(path, query, keep) {
52
+ async raw(path, query, keep, post) {
53
53
  const f = this.c.fetch ?? fetch;
54
54
  const url = new URL(this.c.url.replace(/\/+$/, "") + path);
55
55
  for (const [k, v] of Object.entries(query))
@@ -70,7 +70,13 @@ class Http {
70
70
  let bytes;
71
71
  try {
72
72
  response = await f(url, {
73
- headers: { authorization: `Bearer ${this.c.apiKey}` },
73
+ headers: {
74
+ authorization: `Bearer ${this.c.apiKey}`,
75
+ ...(post !== undefined
76
+ ? { "content-type": "application/json" }
77
+ : {}),
78
+ },
79
+ ...(post !== undefined ? { method: "POST", body: post } : {}),
74
80
  ...(this.c.signal ? { signal: this.c.signal } : {}),
75
81
  });
76
82
  bytes = new Uint8Array(await response.arrayBuffer());
@@ -141,8 +147,8 @@ class Http {
141
147
  throw new ExportError(`${response.status} ${code}: ${message}`, code, response.status);
142
148
  }
143
149
  }
144
- async get(path, query, keep) {
145
- const answer = await this.raw(path, query, keep);
150
+ async get(path, query, keep, post) {
151
+ const answer = await this.raw(path, query, keep, post);
146
152
  const text = new TextDecoder().decode(answer.bytes);
147
153
  try {
148
154
  return {
@@ -338,6 +344,7 @@ const RECORDED = new Set([
338
344
  "invalid_amount",
339
345
  "invalid_cursor",
340
346
  "usd_volume_unavailable",
347
+ "invalid_quote",
341
348
  "partial_coverage",
342
349
  "query_work_limit",
343
350
  ]);
@@ -393,6 +400,45 @@ export async function exportTotals(connection, out, mint, quote = null) {
393
400
  save(connection, out, ["info", infoName(mint)], info, content(info), "tokenInfo");
394
401
  return totals;
395
402
  }
403
+ export async function exportCompute(connection, out, plan) {
404
+ const http = new Http(connection);
405
+ mkdirSync(join(out, "compute"), { recursive: true });
406
+ let answer;
407
+ try {
408
+ const { status, body } = await http.get("/v1/functions/compute", {}, recordable, JSON.stringify({ plan }));
409
+ if (status === 200)
410
+ answer = { ok: body };
411
+ else {
412
+ const e = body.error;
413
+ const retry = body.retryAfterMs;
414
+ answer = {
415
+ code: e.code,
416
+ error: {
417
+ error: { kind: e.kind, code: e.code, message: e.message },
418
+ ...(typeof retry === "number" ? { retryAfterMs: retry } : {}),
419
+ },
420
+ };
421
+ }
422
+ }
423
+ catch (error) {
424
+ if (error instanceof ExportError && error.status === 404)
425
+ throw new ExportError("This server does not run ctx.compute for local runs yet (it answered 404): it must be upgraded first.", "unsupported_server", 404);
426
+ throw error;
427
+ }
428
+ save(connection, out, ["compute", `${computeName(plan)}.json`], answer, "ok" in answer
429
+ ? { plan, result: answer.ok }
430
+ : { plan, ...answer.error }, "compute");
431
+ const ok = "ok" in answer ? answer.ok : null;
432
+ return {
433
+ rows: ok === null
434
+ ? null
435
+ : Array.isArray(ok)
436
+ ? ok.length
437
+ : typeof ok.length === "number"
438
+ ? ok.length
439
+ : null,
440
+ };
441
+ }
396
442
  export async function exportVolumeSearch(connection, out, params) {
397
443
  const http = new Http(connection);
398
444
  mkdirSync(join(out, "volume"), { recursive: true });
package/dist/limits.d.ts CHANGED
@@ -11,7 +11,11 @@ export declare const execLimits: {
11
11
  * reads excluded: they count in `readMs`.
12
12
  */
13
13
  readonly cpuMs: 10000;
14
- /** Wall time, waits (`ctx.jobs.wait`, host operations) included. */
14
+ /**
15
+ * Wall time, waits (`ctx.jobs.wait`, host operations, timers) included.
16
+ * Time spent waiting for history reads (`ctx.data.*`) does not count, up
17
+ * to `readWaitMs`.
18
+ */
15
19
  readonly wallMs: 60000;
16
20
  readonly heapBytes: number;
17
21
  /** JSON arguments of a direct call or a job. */
@@ -34,6 +38,16 @@ export declare const execLimits: {
34
38
  readonly readMs: 30000;
35
39
  /** User storage bytes read plus written by one execution (`io_limit`). */
36
40
  readonly storageBytes: number;
41
+ /**
42
+ * History read wait not counted in `wallMs`, at most: an execution lasts
43
+ * `wallMs + readWaitMs` (15 minutes) at most.
44
+ */
45
+ readonly readWaitMs: number;
46
+ /**
47
+ * Of one history read call, at most this much is given back to `wallMs`:
48
+ * a read that hangs still ends at the wall limit.
49
+ */
50
+ readonly readCallWaitMs: 120000;
37
51
  };
38
52
  /** User storage (`ctx.storage`), per account. Server defaults; operators may change them. */
39
53
  export declare const storageLimits: {
package/dist/limits.js CHANGED
@@ -11,6 +11,8 @@ export const execLimits = {
11
11
  hostOps: 10_000,
12
12
  readMs: 30_000,
13
13
  storageBytes: 1024 * MIB,
14
+ readWaitMs: 14 * 60_000,
15
+ readCallWaitMs: 120_000,
14
16
  };
15
17
  export const storageLimits = {
16
18
  shortQuotaBytes: 1024 * MIB,
@@ -106,9 +106,17 @@ export declare class LocalData {
106
106
  infoFailure(mint: string): ReadFailure | undefined;
107
107
  /** The error a volume search page answered at export, or `undefined`. */
108
108
  volumeFailure(params: readonly (readonly [string, string])[]): ReadFailure | undefined;
109
+ /** The answer `ctx.compute(plan)` gave at export (`zyrk export --compute`). */
110
+ computeResult(plan: unknown): Json | undefined;
111
+ /** The error `ctx.compute(plan)` answered at export, or `undefined`. */
112
+ computeFailure(plan: unknown): ReadFailure | undefined;
109
113
  /** A volume search page saved by the export for exactly these parameters. */
110
114
  volumePage(params: readonly (readonly [string, string])[]): Json | undefined;
111
115
  }
116
+ /** JSON with object keys sorted at every level (the name of a plan). */
117
+ export declare function canonical(v: unknown): string;
118
+ /** File name of a saved compute answer. */
119
+ export declare function computeName(plan: unknown): string;
112
120
  export declare const totalsName: (mint: string, quote: string | null) => string;
113
121
  export declare const infoName: (mint: string) => string;
114
122
  /** Parameters as the sandbox passes them (`[key, string value]`), sorted. */
@@ -360,11 +360,34 @@ export class LocalData {
360
360
  volumeFailure(params) {
361
361
  return this.#failure(join("volume", `${volumeName(params)}.json`), "zyrk export --volume-search '<params>'");
362
362
  }
363
+ computeResult(plan) {
364
+ const saved = this.#json(join(this.dir, "compute", `${computeName(plan)}.json`), "zyrk export --compute '<plan>'");
365
+ return saved?.result;
366
+ }
367
+ computeFailure(plan) {
368
+ return this.#failure(join("compute", `${computeName(plan)}.json`), "zyrk export --compute '<plan>'");
369
+ }
363
370
  volumePage(params) {
364
371
  const saved = this.#json(join(this.dir, "volume", `${volumeName(params)}.json`), "zyrk export --volume-search '<params>'");
365
372
  return saved?.page;
366
373
  }
367
374
  }
375
+ export function canonical(v) {
376
+ if (Array.isArray(v))
377
+ return `[${v.map(canonical).join(",")}]`;
378
+ if (v !== null && typeof v === "object")
379
+ return `{${Object.keys(v)
380
+ .sort()
381
+ .map((k) => `${JSON.stringify(k)}:${canonical(v[k])}`)
382
+ .join(",")}}`;
383
+ return JSON.stringify(v) ?? "null";
384
+ }
385
+ export function computeName(plan) {
386
+ return createHash("sha256")
387
+ .update(canonical(plan))
388
+ .digest("hex")
389
+ .slice(0, 32);
390
+ }
368
391
  export const totalsName = (mint, quote) => quote ? `${mint}~${quote}.json` : `${mint}.json`;
369
392
  export const infoName = (mint) => `${mint}.json`;
370
393
  export function volumeName(params) {
package/dist/local.js CHANGED
@@ -6,7 +6,7 @@ import { getHeapStatistics } from "node:v8";
6
6
  import vm from "node:vm";
7
7
  import { buildProgram } from "./build.js";
8
8
  import { DELIVERY_BATCH, REPLAY_MAX_BATCH, MISSING_OPERATION_FIELDS, intervalEvents, replayEvents, } from "./local-replay.js";
9
- import { LocalData, ReadFailed, spec as readSpec, } from "./local-data.js";
9
+ import { LocalData, canonical, ReadFailed, spec as readSpec, } from "./local-data.js";
10
10
  import { LocalStorage, StorageOpError, } from "./local-storage.js";
11
11
  import { accountLimits, execLimits, subscriptionLimits } from "./limits.js";
12
12
  const here = dirname(fileURLToPath(import.meta.url));
@@ -21,6 +21,20 @@ function batchReadsFromEnv() {
21
21
  }
22
22
  return on;
23
23
  }
24
+ const COMPUTE_OUTPUT_BYTES = 64 * 2 ** 20;
25
+ const computeLine = (r, row) => {
26
+ const parts = [...r.keys, ...r.measures].map((n) => {
27
+ const c = r.columns[n];
28
+ const v = c.values[row];
29
+ const value = c.type === "f64"
30
+ ? typeof v === "number" && Number.isFinite(v)
31
+ ? v
32
+ : null
33
+ : v;
34
+ return `${JSON.stringify(n)}:${JSON.stringify(value)}`;
35
+ });
36
+ return `{${parts.join(",")}}\n`;
37
+ };
24
38
  let bootstrapText;
25
39
  export function sandboxBootstrap() {
26
40
  return (bootstrapText ??= readFileSync(join(here, "sandbox-bootstrap.js"), "utf8"));
@@ -230,6 +244,7 @@ export class LocalRunner {
230
244
  pending: 0,
231
245
  io: { bytes: 0 },
232
246
  startedAt: 0,
247
+ readWaitMs: 0,
233
248
  done,
234
249
  finish,
235
250
  };
@@ -370,7 +385,7 @@ export class LocalRunner {
370
385
  if (e.usage.outputBytes + size > this.limits.exec.outputBytes * e.scale)
371
386
  throw limit("output_limit", `Result, logs and emits went over ${this.limits.exec.outputBytes * e.scale} bytes`);
372
387
  e.usage.outputBytes += size;
373
- if (performance.now() - e.startedAt > this.limits.exec.wallMs)
388
+ if (this.#wallUsed(e) > this.limits.exec.wallMs)
374
389
  throw limit("wall_limit", `The execution ran over ${this.limits.exec.wallMs} ms (measured locally, not preempted)`);
375
390
  if (e.cancelled)
376
391
  throw new HostError("permission", "cancelled", "Its parent failed: the job was cancelled");
@@ -446,6 +461,38 @@ export class LocalRunner {
446
461
  };
447
462
  const fromRead = (f) => new HostError(kindOfSdkCode(f.code), f.code, f.message, f.retryAfterMs);
448
463
  const guestJson = (v) => realm.JSON.parse(JSON.stringify(v) ?? "null");
464
+ const computeOutputs = new Map();
465
+ let computeNext = 0;
466
+ const computeSummary = (r) => ({
467
+ length: r.length,
468
+ keys: r.keys,
469
+ measures: r.measures,
470
+ coverage: r.coverage,
471
+ complete: r.complete,
472
+ skipped: r.skipped,
473
+ stats: r.stats,
474
+ });
475
+ const computeObject = (r) => {
476
+ const o = guestJson(computeSummary(r));
477
+ const columns = new realm.Object();
478
+ for (const n of [...r.keys, ...r.measures]) {
479
+ const c = r.columns[n];
480
+ columns[n] =
481
+ c.type === "f64"
482
+ ? realm.Float64Array.from(c.values.map((v) => v === "NaN"
483
+ ? NaN
484
+ : v === "Infinity"
485
+ ? Infinity
486
+ : v === "-Infinity"
487
+ ? -Infinity
488
+ : Number(v)))
489
+ : c.type === "int"
490
+ ? realm.Array.from(c.values.map((v) => (v === null ? null : BigInt(String(v)))))
491
+ : realm.Array.from(c.values);
492
+ }
493
+ o.columns = columns;
494
+ return o;
495
+ };
449
496
  const ms = (v) => typeof v === "number" && Number.isFinite(v) && Math.abs(v) < 9e15
450
497
  ? Math.trunc(v)
451
498
  : undefined;
@@ -472,6 +519,9 @@ export class LocalRunner {
472
519
  wf.readBytes > this.limits.account.workflowReadBytes)
473
520
  throw readLimit();
474
521
  };
522
+ const waited = (t0) => {
523
+ e.readWaitMs += Math.min(performance.now() - t0, this.limits.exec.readCallWaitMs);
524
+ };
475
525
  const data = () => {
476
526
  if (!this.data)
477
527
  throw sdkError(program("local_data_missing", "No data directory: export history with zyrk export, then pass --data (or { data })."));
@@ -480,6 +530,7 @@ export class LocalRunner {
480
530
  const saved = (answer, failure, missing) => {
481
531
  let value;
482
532
  let failed;
533
+ const t0 = performance.now();
483
534
  try {
484
535
  value = answer();
485
536
  failed = value === undefined ? failure() : undefined;
@@ -489,6 +540,9 @@ export class LocalRunner {
489
540
  throw sdkError(fromRead(error.failure));
490
541
  throw error;
491
542
  }
543
+ finally {
544
+ waited(t0);
545
+ }
492
546
  if (failed)
493
547
  throw sdkError(fromRead(failed));
494
548
  if (value === undefined)
@@ -541,6 +595,7 @@ export class LocalRunner {
541
595
  }
542
596
  finally {
543
597
  e.usage.readMs += performance.now() - t0;
598
+ waited(t0);
544
599
  sample();
545
600
  }
546
601
  if (hour === null)
@@ -567,6 +622,63 @@ export class LocalRunner {
567
622
  throw sdkError(program("invalid_argument", "Expected a mint address"));
568
623
  return saved(() => data().info(mint), () => data().infoFailure(mint), `No exported info for ${mint} (exports by older versions saved only its totals): zyrk export --totals ${mint}`);
569
624
  },
625
+ computeAsk: () => realm.Promise.resolve(),
626
+ compute: (text, toStorage) => {
627
+ readAllowed();
628
+ e.usage.readCalls++;
629
+ let plan;
630
+ try {
631
+ plan = JSON.parse(String(text));
632
+ }
633
+ catch {
634
+ throw sdkError(program("invalid_plan", "The plan is not JSON"));
635
+ }
636
+ const answer = saved(() => data().computeResult(plan), () => data().computeFailure(plan), `No exported result for this plan: zyrk export --compute '${canonical(plan)}'`);
637
+ if (toStorage === true) {
638
+ if (computeOutputs.size > 0)
639
+ throw sdkError(program("invalid_argument", "One ctx.compute output at a time per execution: await the one being written first"));
640
+ if (Array.isArray(answer))
641
+ throw sdkError(program("invalid_plan", "output writes the result of one plan"));
642
+ const result = answer;
643
+ let bytes = 0;
644
+ for (let row = 0; row < result.length; row++)
645
+ bytes += Buffer.byteLength(computeLine(result, row), "utf8");
646
+ if (bytes > COMPUTE_OUTPUT_BYTES)
647
+ throw sdkError(limit("compute_result_limit", `the result is ${Math.ceil(bytes / 2 ** 20)} MiB of JSON lines (${result.length} rows), more than the ${COMPUTE_OUTPUT_BYTES / 2 ** 20} MiB of one storage object: add a limit, a top or a having, group less finely, or split the range into several calls`));
648
+ const handle = computeNext++;
649
+ computeOutputs.set(handle, {
650
+ result: answer,
651
+ row: 0,
652
+ });
653
+ return guestJson({
654
+ handle,
655
+ summary: computeSummary(answer),
656
+ });
657
+ }
658
+ return Array.isArray(answer)
659
+ ? realm.Array.from(answer.map((a) => computeObject(a)))
660
+ : computeObject(answer);
661
+ },
662
+ computeChunk: (handle) => {
663
+ const o = computeOutputs.get(Number(handle));
664
+ if (!o)
665
+ throw sdkError(program("invalid_argument", "Unknown compute output"));
666
+ const r = o.result;
667
+ if (o.row >= r.length)
668
+ return null;
669
+ let text = "";
670
+ while (o.row < r.length && text.length < 4 * 1024 * 1024 - 64 * 1024) {
671
+ text += computeLine(r, o.row);
672
+ o.row++;
673
+ }
674
+ const bytes = Buffer.from(text, "utf8");
675
+ const out = new realm.Uint8Array(bytes.length);
676
+ out.set(bytes);
677
+ return out;
678
+ },
679
+ computeDrop: (handle) => {
680
+ computeOutputs.delete(Number(handle));
681
+ },
570
682
  volume: (text) => {
571
683
  readAllowed();
572
684
  e.usage.readCalls++;
@@ -602,8 +714,13 @@ export class LocalRunner {
602
714
  },
603
715
  };
604
716
  }
717
+ #wallUsed(e) {
718
+ return (performance.now() -
719
+ e.startedAt -
720
+ Math.min(e.readWaitMs, this.limits.exec.readWaitMs));
721
+ }
605
722
  #checkWall(e) {
606
- if (performance.now() - e.startedAt > this.limits.exec.wallMs) {
723
+ if (this.#wallUsed(e) > this.limits.exec.wallMs) {
607
724
  e.fatal ??= limit("wall_limit", `The execution ran over ${this.limits.exec.wallMs} ms`);
608
725
  throw e.fatal;
609
726
  }
@@ -795,7 +912,7 @@ export class LocalRunner {
795
912
  });
796
913
  if (this.#waiting >= account.parallelExecutions - 1)
797
914
  throw limit("job_wait_limit", `At most ${account.parallelExecutions - 1} executions wait at once`);
798
- const left = this.limits.exec.wallMs - (performance.now() - e.startedAt);
915
+ const left = this.limits.exec.wallMs - this.#wallUsed(e);
799
916
  const timeout = Math.max(0, Math.min(typeof a.timeout_ms === "number" ? a.timeout_ms : left, left));
800
917
  this.#waiting++;
801
918
  let timer;
@@ -427,6 +427,65 @@
427
427
  long: storageArea("long"),
428
428
  durable: storageArea("durable")
429
429
  });
430
+ const computeAreas = ["short", "long", "durable"];
431
+ let outputTurn = PromiseCtor.resolve();
432
+ const compute = async (plan, options) => {
433
+ if (plan === null || typeof plan !== "object")
434
+ throw fail("invalid_plan", "Expected a plan object (or an array of plans)");
435
+ let output;
436
+ if (options !== void 0 && options !== null) {
437
+ if (typeOf(options) !== "object") throw fail("invalid_argument", "Expected options { output }");
438
+ for (const k of keys(options))
439
+ if (k !== "output") throw fail("invalid_argument", "Unknown compute option: " + k);
440
+ output = options.output;
441
+ if (output !== void 0 && (typeOf(output) !== "object" || !computeAreas.includes(output.area) || typeof output.key !== "string"))
442
+ throw fail("invalid_argument", 'output is { area: "short" | "long" | "durable", key }');
443
+ }
444
+ let text;
445
+ try {
446
+ text = stringify(plan);
447
+ } catch {
448
+ throw fail("invalid_plan", "The plan is not JSON (write integers beyond 2^53 as decimal strings)");
449
+ }
450
+ await N.computeAsk(text);
451
+ if (output === void 0) return N.compute(text, false);
452
+ const before = outputTurn;
453
+ let release;
454
+ outputTurn = new PromiseCtor((resolve) => {
455
+ release = resolve;
456
+ });
457
+ try {
458
+ await before;
459
+ return await computeOutput(text, output);
460
+ } finally {
461
+ release();
462
+ }
463
+ };
464
+ const computeOutput = async (text, output) => {
465
+ const r = N.compute(text, true);
466
+ let w;
467
+ try {
468
+ w = await storage[output.area].writer(output.key, { contentType: "application/x-ndjson" });
469
+ let bytes = 0;
470
+ for (; ; ) {
471
+ const chunk = N.computeChunk(r.handle);
472
+ if (chunk === null) break;
473
+ bytes += chunk.length;
474
+ await w.write(chunk);
475
+ }
476
+ const head2 = await w.close();
477
+ return freeze({
478
+ ...r.summary,
479
+ output: freeze({ area: output.area, key: output.key, rows: r.summary.length, bytes, etag: head2.etag })
480
+ });
481
+ } catch (e) {
482
+ if (w) await w.abort().catch(() => {
483
+ });
484
+ throw e;
485
+ } finally {
486
+ N.computeDrop(r.handle);
487
+ }
488
+ };
430
489
  const start = (functionName, argsText, executionText) => {
431
490
  const p = program();
432
491
  let depth = 0;
@@ -487,6 +546,7 @@
487
546
  continue: (args) => call({ Continue: { args } })
488
547
  }),
489
548
  storage,
549
+ compute,
490
550
  subscriptions: freeze({
491
551
  set: (name, subscription) => call({ Subscribe: { name, subscription } }),
492
552
  remove: (name) => call({ Unsubscribe: { name } }),
package/dist/sdk.d.ts CHANGED
@@ -388,6 +388,12 @@ export type TokenMetadata = {
388
388
  readonly quoteName: string | null;
389
389
  readonly quoteSymbol: string | null;
390
390
  readonly quoteUri: string | null;
391
+ /**
392
+ * Decimals of `quoteMint`: the row's, else 9 for SOL and 6 for USDC, else
393
+ * those of the quote table (rows sent before 2026-10-29 18:15 UTC lack
394
+ * them), else `null` (absent from metadata saved by older exports).
395
+ */
396
+ readonly quoteDecimals?: number | null;
391
397
  /** Time, slot and signature of the row the values come from. */
392
398
  readonly asOfMs: number | null;
393
399
  readonly slot: number | null;
@@ -930,6 +936,189 @@ export interface StorageArea {
930
936
  putJSON(key: string, value: JsonInput, options?: Omit<StoragePutOptions, "contentType">): Promise<StorageObjectHead>;
931
937
  writer(key: string, options?: StoragePutOptions): Promise<StorageWriter>;
932
938
  }
939
+ /** Milliseconds, or text such as `"500ms"`, `"30s"`, `"15m"`, `"6h"`, `"30d"`. */
940
+ export type ComputeDuration = number | `${number}${"ms" | "s" | "m" | "h" | "d"}`;
941
+ /** `[since, until)` in ms; `until` defaults to the call's time. */
942
+ export type ComputeWindow = {
943
+ readonly since: number;
944
+ readonly until?: number;
945
+ } | {
946
+ readonly last: ComputeDuration;
947
+ readonly until?: number;
948
+ };
949
+ /** Fields of a trade a filter or a measure reads. */
950
+ export type ComputeField = "side" | "protocol" | "token" | "trader" | "quoteMint" | "timeMs" | "ageMs" | "quoteRaw" | "tokenRaw" | "quoteAmount" | "tokenAmount" | "execPrice" | "isCreator" | "marketFlags" | "orderFlags";
951
+ /** Fields measured by `sum`, `min`, `max`, `avg`, `first`, `last`, `anchor`, `asOf`. */
952
+ export type ComputeNumericField = "timeMs" | "ageMs" | "quoteRaw" | "tokenRaw" | "quoteAmount" | "tokenAmount" | "execPrice";
953
+ type ComputeValue = string | number | boolean;
954
+ /** A comparison of one field: a value (equality), or operators (all must hold). */
955
+ export type ComputeCondition = ComputeValue | {
956
+ readonly eq?: ComputeValue;
957
+ readonly ne?: ComputeValue;
958
+ readonly gt?: number | string;
959
+ readonly gte?: number | string;
960
+ readonly lt?: number | string;
961
+ readonly lte?: number | string;
962
+ readonly in?: readonly ComputeValue[];
963
+ readonly notIn?: readonly ComputeValue[];
964
+ /** Flag fields (`marketFlags`, `orderFlags`): bits of the mask set. */
965
+ readonly hasAll?: number;
966
+ readonly hasAny?: number;
967
+ readonly hasNone?: number;
968
+ };
969
+ /**
970
+ * A filter: every field listed must pass (`and`), with `and`, `or`, `not`
971
+ * to combine. In `where`, the fields of a trade (and `"<stage>.<measure>"`);
972
+ * in `having`, the measures and keys of a group. A comparison with an
973
+ * unknown value (no trader, a creation not seen, a missing stage value) is
974
+ * false, `ne` and `notIn` included.
975
+ */
976
+ export interface ComputeFilter {
977
+ readonly and?: readonly ComputeFilter[];
978
+ readonly or?: readonly ComputeFilter[];
979
+ readonly not?: ComputeFilter;
980
+ readonly [field: string]: ComputeCondition | ComputeFilter | readonly ComputeFilter[] | undefined;
981
+ }
982
+ export type ComputeKey = "token" | "trader" | "quoteMint" | "side" | "protocol"
983
+ /** `floor(timeMs / width) * width`. */
984
+ | {
985
+ readonly time: ComputeDuration;
986
+ }
987
+ /** `floor(ageMs / width) * width` (tokens whose creation is known). */
988
+ | {
989
+ readonly age: ComputeDuration;
990
+ };
991
+ type MeasureOptions = {
992
+ /** Only the trades that pass it. */
993
+ readonly where?: ComputeFilter;
994
+ /** Plans with `anchors`: the token's trades in `[anchor + from, anchor + to)` ms. */
995
+ readonly window?: readonly [number, number];
996
+ };
997
+ export type ComputeMeasure = ({
998
+ readonly count: true;
999
+ } & MeasureOptions) | ({
1000
+ readonly countDistinct: "token" | "trader" | "quoteMint";
1001
+ } & MeasureOptions) | ({
1002
+ readonly sum: ComputeNumericField;
1003
+ } & MeasureOptions) | ({
1004
+ readonly min: ComputeNumericField;
1005
+ } & MeasureOptions) | ({
1006
+ readonly max: ComputeNumericField;
1007
+ } & MeasureOptions) | ({
1008
+ readonly avg: ComputeNumericField;
1009
+ } & MeasureOptions)
1010
+ /** In the token's trade sequence (see the docs). */
1011
+ | ({
1012
+ readonly first: ComputeNumericField;
1013
+ } & MeasureOptions) | ({
1014
+ readonly last: ComputeNumericField;
1015
+ } & MeasureOptions)
1016
+ /** `Number(a) / Number(b)` of two other measures. */
1017
+ | {
1018
+ readonly ratio: readonly [string, string];
1019
+ }
1020
+ /** Plans with `anchors`: a field of the anchor trade. */
1021
+ | {
1022
+ readonly anchor: ComputeNumericField;
1023
+ }
1024
+ /**
1025
+ * Plans with `anchors`: `field` of the token's trade as of `anchor +
1026
+ * offsetMs` (the last at or before it; with `direction: "after"`, the
1027
+ * first at or after it), within `withinMs` (default and at most 1 h). An
1028
+ * array of offsets gives one column each, `"<name>@<offset>"`.
1029
+ */
1030
+ | ({
1031
+ readonly asOf: ComputeNumericField;
1032
+ readonly offsetMs: number | readonly number[];
1033
+ readonly direction?: "before" | "after";
1034
+ readonly withinMs?: number;
1035
+ } & Omit<MeasureOptions, "window">);
1036
+ export interface ComputePlan {
1037
+ readonly range: ComputeWindow;
1038
+ /** Only these mints (1 to 10 000). */
1039
+ readonly tokens?: readonly Address[];
1040
+ /** Only the tokens created in this window (creations seen in its hours). */
1041
+ readonly created?: ComputeWindow;
1042
+ /** `"SOL"` (default), a quote mint, `"auto"` (per token, as `tokenTotals`) or `"any"`. */
1043
+ readonly quote?: "SOL" | "auto" | "any" | Address;
1044
+ readonly where?: ComputeFilter;
1045
+ readonly groupBy?: readonly ComputeKey[];
1046
+ /** 1 to 16 named measures; results list them in name order. */
1047
+ readonly measures: Readonly<Record<string, ComputeMeasure>>;
1048
+ readonly having?: ComputeFilter;
1049
+ /** The `n` groups with the largest `by`, per partition of the `per` keys. */
1050
+ readonly top?: {
1051
+ readonly by: string;
1052
+ readonly n: number;
1053
+ readonly per?: readonly string[];
1054
+ };
1055
+ readonly orderBy?: readonly (string | {
1056
+ readonly by: string;
1057
+ readonly desc?: boolean;
1058
+ })[];
1059
+ readonly limit?: number;
1060
+ /** Accept a window not fully covered, or rejected operations. */
1061
+ readonly allowPartial?: boolean;
1062
+ /** Stages: measures per token, read in `where` as `"<stage>.<measure>"`. */
1063
+ readonly with?: Readonly<Record<string, {
1064
+ readonly where?: ComputeFilter;
1065
+ readonly measures: Readonly<Record<string, ComputeMeasure>>;
1066
+ readonly having?: ComputeFilter;
1067
+ }>>;
1068
+ /**
1069
+ * One row per anchor: the first trade, in its token's sequence, of each
1070
+ * group of `by` (with `token`) that passes `where`. Measures then read it
1071
+ * (`anchor`) and its token's trades around it (`asOf`, `window`).
1072
+ */
1073
+ readonly anchors?: {
1074
+ readonly by: readonly ComputeKey[];
1075
+ readonly where?: ComputeFilter;
1076
+ readonly pick?: "first";
1077
+ };
1078
+ }
1079
+ /** A column of a result: addresses and names, numbers, or exact integers. */
1080
+ export type ComputeColumn = readonly (string | null)[] | Float64Array | readonly (bigint | null)[];
1081
+ export interface ComputeStats {
1082
+ readonly hours: number;
1083
+ readonly derivedHours: number;
1084
+ readonly sealedHours: number;
1085
+ readonly openHours: number;
1086
+ readonly passes: number;
1087
+ readonly trades: number;
1088
+ readonly groups: number;
1089
+ /** Server time of the call, every thread (the history reading; not your CPU). */
1090
+ readonly cpuMs: number;
1091
+ readonly wallMs: number;
1092
+ }
1093
+ interface ComputeSummary {
1094
+ readonly length: number;
1095
+ /** Key columns, in `groupBy` order. */
1096
+ readonly keys: readonly string[];
1097
+ /** Measure columns, in name order. */
1098
+ readonly measures: readonly string[];
1099
+ readonly coverage: Json;
1100
+ /** Covered, and no rejected operation counted. */
1101
+ readonly complete: boolean;
1102
+ /** Operations of the tokens read that normalization rejects, in the range. */
1103
+ readonly skipped: number;
1104
+ readonly stats: ComputeStats;
1105
+ }
1106
+ export interface ComputeResult extends ComputeSummary {
1107
+ /** One column per key and measure, `length` values each. */
1108
+ readonly columns: Readonly<Record<string, ComputeColumn>>;
1109
+ }
1110
+ /** A result written to storage (`output`): JSON lines, one object per row. */
1111
+ export interface ComputeOutput extends ComputeSummary {
1112
+ readonly output: {
1113
+ readonly area: "short" | "long" | "durable";
1114
+ readonly key: string;
1115
+ readonly rows: number;
1116
+ readonly bytes: number;
1117
+ readonly etag: string;
1118
+ };
1119
+ }
1120
+ /** The rows of a result as objects (keys and measures by name). */
1121
+ export declare function computeRows(r: ComputeResult): Record<string, string | number | bigint | null>[];
933
1122
  export interface Context {
934
1123
  readonly execution: ExecutionInfo;
935
1124
  readonly data: {
@@ -943,10 +1132,13 @@ export interface Context {
943
1132
  /**
944
1133
  * Volume of a token since its creation (`cumulativeRaw`, `buyRaw`,
945
1134
  * `sellRaw`, `buys`, `sells`, `sinceCreation.complete`, `coverage`), its
946
- * `creation` and `metadata`, as JSON.
1135
+ * `creation` and `metadata`, as JSON. Amounts are in `quoteMint` at
1136
+ * `quoteDecimals`; without `quote`: SOL; `quote: "auto"`: SOL unless the
1137
+ * token trades in other quotes only (then its most traded one; `quotes`
1138
+ * lists them all).
947
1139
  */
948
1140
  tokenTotals(mint: Address, options?: {
949
- quote?: Address;
1141
+ quote?: Address | "auto";
950
1142
  }): Promise<Json>;
951
1143
  /**
952
1144
  * Latest known metadata and the creation, at most two blocks read (no
@@ -990,6 +1182,22 @@ export interface Context {
990
1182
  * after the last write), `long` (500 MiB, 7 days after the last write) and
991
1183
  * `durable` (50 MiB, never expires).
992
1184
  */
1185
+ /**
1186
+ * Runs a plan (or up to 8 plans over one read) natively over the
1187
+ * history: answers columns, or with `output` writes the rows to storage
1188
+ * as JSON lines and answers a summary. Errors: `invalid_plan`,
1189
+ * `invalid_window`, `partial_coverage`, `compute_result_limit`,
1190
+ * `compute_memory_limit`, `compute_limit`, `compute_overflow`, and those of
1191
+ * history reads.
1192
+ */
1193
+ compute(plan: ComputePlan): Promise<ComputeResult>;
1194
+ compute(plans: readonly ComputePlan[]): Promise<ComputeResult[]>;
1195
+ compute(plan: ComputePlan, options: {
1196
+ readonly output: {
1197
+ readonly area: "short" | "long" | "durable";
1198
+ readonly key: string;
1199
+ };
1200
+ }): Promise<ComputeOutput>;
993
1201
  readonly storage: {
994
1202
  readonly short: StorageArea;
995
1203
  readonly long: StorageArea;
package/dist/sdk.js CHANGED
@@ -73,6 +73,18 @@ export function isCompletion(args) {
73
73
  args.kind === "completion" &&
74
74
  typeof args.executionId === "string");
75
75
  }
76
+ export function computeRows(r) {
77
+ const names = [...r.keys, ...r.measures];
78
+ const out = [];
79
+ for (let i = 0; i < r.length; i++) {
80
+ const row = {};
81
+ for (const n of names)
82
+ row[n] =
83
+ r.columns[n][i] ?? null;
84
+ out.push(row);
85
+ }
86
+ return out;
87
+ }
76
88
  export const SDK_MARKER = "__zyrkSdk";
77
89
  export const SDK_VERSION = "0.2";
78
90
  export function defineProgram(program) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@johannc-dev/zyrk",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "SDK and CLI for Zyrk hosted functions",
5
5
  "license": "UNLICENSED",
6
6
  "publishConfig": {