@johannc-dev/zyrk 0.0.0-stage → 0.2.1

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.
@@ -0,0 +1,96 @@
1
+ import { type ReadMeta } from "./local-data.js";
2
+ import type { Json, VolumeSearchParams } from "./sdk.js";
3
+ export interface ExportConnection {
4
+ /** Gateway URL (`TH_API_URL`). */
5
+ readonly url: string;
6
+ /** API key (`TH_API_KEY`) of an account with hosted functions. */
7
+ readonly apiKey: string;
8
+ /** At most this many requests per minute (default: as fast as the server allows). */
9
+ readonly rpm?: number;
10
+ /** Network failures, 502, 503 and 504 tolerated in a row before giving up (default 8); another 5xx is retried once. */
11
+ readonly maxRetries?: number;
12
+ /** Longest wait accepted on 429s in a row before stopping (default 5 minutes): a daily quota stops the export. */
13
+ readonly maxWaitMs?: number;
14
+ readonly fetch?: typeof fetch;
15
+ readonly signal?: AbortSignal;
16
+ readonly onProgress?: (progress: ExportProgress) => void;
17
+ /**
18
+ * Hours that may still change (open, not sealed), files downloaded again,
19
+ * errors saved as the answer of a call.
20
+ */
21
+ readonly onWarning?: (message: string) => void;
22
+ }
23
+ export interface ExportProgress {
24
+ readonly read: string;
25
+ readonly hour: number | null;
26
+ readonly hours: number;
27
+ readonly done: boolean;
28
+ readonly bytes: number;
29
+ }
30
+ export interface ExportRead {
31
+ readonly scope: "token" | "wallet";
32
+ readonly key: string;
33
+ readonly sinceMs: number;
34
+ readonly untilMs: number;
35
+ readonly side?: "any" | "buy" | "sell";
36
+ readonly withSignatures?: boolean;
37
+ readonly withMarket?: boolean;
38
+ readonly withEvents?: boolean;
39
+ readonly withOrder?: boolean;
40
+ readonly withTx?: boolean;
41
+ /** Download every hour again, even those of a finished export. */
42
+ readonly refresh?: boolean;
43
+ }
44
+ export declare class ExportError extends Error {
45
+ readonly code: string;
46
+ readonly status?: number | undefined;
47
+ readonly name = "ExportError";
48
+ constructor(message: string, code: string, status?: number | undefined);
49
+ }
50
+ export interface ExportSummary {
51
+ readonly reads: readonly ReadMeta[];
52
+ readonly requests: number;
53
+ /** Compressed bytes written. */
54
+ readonly bytes: number;
55
+ }
56
+ /** `[since, until)` in parts of at most 30 days, ending on UTC day boundaries. */
57
+ export declare function parts(sinceMs: number, untilMs: number): [number, number][];
58
+ /** Meta of an export answer. */
59
+ interface HourMeta {
60
+ readonly format?: string;
61
+ readonly coverage?: ReadMeta["coverage"];
62
+ readonly batch?: {
63
+ hour: number;
64
+ zstdBytes: number;
65
+ } | null;
66
+ readonly resumeAfterHour?: number | null;
67
+ readonly done?: boolean;
68
+ }
69
+ /** `"ZXH1" | u32 LE meta length | meta JSON | exported hour`. */
70
+ export declare function parseFrame(bytes: Uint8Array): {
71
+ meta: HourMeta;
72
+ file: Uint8Array;
73
+ };
74
+ /**
75
+ * The first hour to download again in a finished export: an hour that was
76
+ * open (`live`) or not sealed yet when it was exported may have changed.
77
+ * `null`: nothing to refresh.
78
+ */
79
+ export declare function staleFrom(meta: ReadMeta, dir: string): number | null;
80
+ /** Exports one read (cut into parts of at most 30 days). Resumes an interrupted export. */
81
+ export declare function exportHistory(connection: ExportConnection, out: string, read: ExportRead): Promise<ExportSummary>;
82
+ /**
83
+ * Saves the totals of a mint for `ctx.data.tokenTotals`, and its info for
84
+ * `ctx.data.tokenInfo`, as hosted programs receive them. Returns the totals,
85
+ * or the error answer saved for them (`{ error: { kind, code, message } }`).
86
+ */
87
+ export declare function exportTotals(connection: ExportConnection, out: string, mint: string, quote?: string | null): Promise<Json>;
88
+ /**
89
+ * Saves every page of a volume search for `params`, following the
90
+ * cursor exactly as `ctx.data.volumeSearch` does, so that the same call in
91
+ * a local run finds each page. A page answered by an error (an invalid
92
+ * parameter...) is saved as that error, and ends the search, as it does in a
93
+ * hosted call.
94
+ */
95
+ export declare function exportVolumeSearch(connection: ExportConnection, out: string, params: VolumeSearchParams): Promise<number>;
96
+ export {};
package/dist/export.js ADDED
@@ -0,0 +1,422 @@
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { decompressHour } from "./hourfile.js";
4
+ import { HOUR_MS, READ_FORMAT, infoName, sha256, readId, spec as readSpec, totalsName, volumeName, } from "./local-data.js";
5
+ import { MAX_WINDOW_MS } from "./limits.js";
6
+ const DAY_MS = 86_400_000;
7
+ const PART_MS = 30 * DAY_MS;
8
+ export class ExportError extends Error {
9
+ code;
10
+ status;
11
+ name = "ExportError";
12
+ constructor(message, code, status) {
13
+ super(message);
14
+ this.code = code;
15
+ this.status = status;
16
+ }
17
+ }
18
+ export function parts(sinceMs, untilMs) {
19
+ if (!(untilMs > sinceMs) || sinceMs < 0)
20
+ throw new ExportError("--until must be after --since", "invalid_window");
21
+ const out = [];
22
+ for (let a = sinceMs; a < untilMs;) {
23
+ const b = Math.min(untilMs, Math.floor(a / DAY_MS) * DAY_MS + PART_MS);
24
+ out.push([a, b]);
25
+ a = b;
26
+ }
27
+ return out;
28
+ }
29
+ const sleep = (ms, signal) => new Promise((resolve, reject) => {
30
+ if (signal?.aborted)
31
+ return reject(signal.reason);
32
+ const t = setTimeout(resolve, ms);
33
+ signal?.addEventListener("abort", () => {
34
+ clearTimeout(t);
35
+ reject(signal.reason);
36
+ }, { once: true });
37
+ });
38
+ const writeAtomic = (file, data) => {
39
+ const tmp = `${file}.tmp-${process.pid}`;
40
+ writeFileSync(tmp, data);
41
+ renameSync(tmp, file);
42
+ };
43
+ const MAX_BUSY_ATTEMPTS = 12;
44
+ const MAX_SERVER_ERRORS = 1;
45
+ class Http {
46
+ c;
47
+ requests = 0;
48
+ #last = 0;
49
+ constructor(c) {
50
+ this.c = c;
51
+ }
52
+ async raw(path, query, keep) {
53
+ const f = this.c.fetch ?? fetch;
54
+ const url = new URL(this.c.url.replace(/\/+$/, "") + path);
55
+ for (const [k, v] of Object.entries(query))
56
+ url.searchParams.set(k, v);
57
+ let failures = 0;
58
+ let serverErrors = 0;
59
+ let busy = 0;
60
+ let waited = 0;
61
+ for (;;) {
62
+ if (this.c.rpm) {
63
+ const gap = 60_000 / this.c.rpm - (Date.now() - this.#last);
64
+ if (gap > 0)
65
+ await sleep(gap, this.c.signal);
66
+ }
67
+ this.#last = Date.now();
68
+ this.requests++;
69
+ let response;
70
+ let bytes;
71
+ try {
72
+ response = await f(url, {
73
+ headers: { authorization: `Bearer ${this.c.apiKey}` },
74
+ ...(this.c.signal ? { signal: this.c.signal } : {}),
75
+ });
76
+ bytes = new Uint8Array(await response.arrayBuffer());
77
+ }
78
+ catch (error) {
79
+ if (this.c.signal?.aborted)
80
+ throw error;
81
+ if (++failures > (this.c.maxRetries ?? 8))
82
+ throw new ExportError(`Network error on ${url.pathname}: ${String(error)}`, "network");
83
+ await sleep(Math.min(30_000, 500 * 2 ** failures), this.c.signal);
84
+ continue;
85
+ }
86
+ const contentType = response.headers.get("content-type") ?? "";
87
+ if (response.ok)
88
+ return { status: response.status, contentType, bytes };
89
+ let body = null;
90
+ try {
91
+ body = JSON.parse(new TextDecoder().decode(bytes));
92
+ }
93
+ catch {
94
+ body = null;
95
+ }
96
+ if (keep?.(response.status, body))
97
+ return { status: response.status, contentType, bytes };
98
+ const err = body?.error;
99
+ const code = typeof err === "string"
100
+ ? err
101
+ : typeof err?.code === "string"
102
+ ? err.code
103
+ : `http_${response.status}`;
104
+ const message = err?.message ?? code;
105
+ const after = response.headers.get("retry-after");
106
+ const header = after === null ? Number.NaN : Number(after);
107
+ const bodyRetry = body
108
+ ?.retryAfterMs;
109
+ const retryMs = typeof bodyRetry === "number"
110
+ ? bodyRetry
111
+ : Number.isFinite(header) && header >= 0
112
+ ? header * 1000
113
+ : undefined;
114
+ if (response.status === 429) {
115
+ const maxWait = this.c.maxWaitMs ?? 300_000;
116
+ const asked = retryMs ?? 60_000;
117
+ if (asked > maxWait)
118
+ throw new ExportError(`${code}: ${message} Retry after ${Math.round(asked / 1000)} s: run the same command again then, the export resumes where it stopped.`, code, 429);
119
+ const wait = Math.max(asked, Math.min(30_000, 1000 * 2 ** busy));
120
+ if (++busy >= MAX_BUSY_ATTEMPTS || waited + wait > maxWait)
121
+ throw new ExportError(`${code}: ${message} The server answered 429 ${busy} times in a row (${Math.round(waited / 1000)} s): run the same command again later, the export resumes where it stopped.`, code, 429);
122
+ waited += wait;
123
+ await sleep(wait, this.c.signal);
124
+ continue;
125
+ }
126
+ if (response.status >= 500 &&
127
+ response.status !== 502 &&
128
+ response.status !== 503 &&
129
+ response.status !== 504) {
130
+ if (++serverErrors > MAX_SERVER_ERRORS)
131
+ throw new ExportError(`${response.status} ${code}: ${message}`, code, response.status);
132
+ await sleep(Math.min(30_000, 500 * 2 ** serverErrors), this.c.signal);
133
+ continue;
134
+ }
135
+ if (response.status >= 500 && code !== "history_timeout") {
136
+ if (++failures > (this.c.maxRetries ?? 8))
137
+ throw new ExportError(`${response.status} ${code}: ${message}`, code, response.status);
138
+ await sleep(retryMs ?? Math.min(30_000, 500 * 2 ** failures), this.c.signal);
139
+ continue;
140
+ }
141
+ throw new ExportError(`${response.status} ${code}: ${message}`, code, response.status);
142
+ }
143
+ }
144
+ async get(path, query, keep) {
145
+ const answer = await this.raw(path, query, keep);
146
+ const text = new TextDecoder().decode(answer.bytes);
147
+ try {
148
+ return {
149
+ status: answer.status,
150
+ body: text ? JSON.parse(text) : null,
151
+ };
152
+ }
153
+ catch {
154
+ throw new ExportError(`${path}: the answer is not JSON`, "decode");
155
+ }
156
+ }
157
+ }
158
+ export function parseFrame(bytes) {
159
+ const magic = String.fromCharCode(...bytes.subarray(0, 4));
160
+ if (bytes.length < 8 || magic !== "ZXH1")
161
+ throw new ExportError("Unexpected answer from the history route (not an export frame)", "decode");
162
+ const n = new DataView(bytes.buffer, bytes.byteOffset).getUint32(4, true);
163
+ if (8 + n > bytes.length)
164
+ throw new ExportError("Truncated answer of the history route", "decode");
165
+ const meta = JSON.parse(new TextDecoder().decode(bytes.subarray(8, 8 + n)));
166
+ if (meta.format !== "zyrk-hour/2")
167
+ throw new ExportError(`Unexpected answer from the history route (format ${String(meta.format)}): update the SDK`, "decode");
168
+ const file = bytes.subarray(8 + n);
169
+ if (meta.batch && file.length !== meta.batch.zstdBytes)
170
+ throw new ExportError("Truncated answer of the history route", "decode");
171
+ return { meta, file };
172
+ }
173
+ function loadMeta(file) {
174
+ if (!existsSync(file))
175
+ return undefined;
176
+ try {
177
+ return JSON.parse(readFileSync(file, "utf8"));
178
+ }
179
+ catch {
180
+ return undefined;
181
+ }
182
+ }
183
+ function cleanTemporaries(dir) {
184
+ for (const name of readdirSync(dir))
185
+ if (/\.tmp-\d+$/.test(name))
186
+ rmSync(join(dir, name), { force: true });
187
+ }
188
+ export function staleFrom(meta, dir) {
189
+ const sealed = meta.coverage?.sealedUntilMs ?? 0;
190
+ let from = sealed < meta.untilMs || meta.coverage?.preparing
191
+ ? Math.floor(Math.max(sealed, meta.sinceMs) / HOUR_MS) - 1
192
+ : null;
193
+ for (const hour of meta.hours) {
194
+ if (from !== null && hour >= from)
195
+ break;
196
+ let live = false;
197
+ try {
198
+ const { header } = decompressHour(readFileSync(join(dir, `${hour}.zhb`)));
199
+ live =
200
+ header.source === "live" || (header.market && !header.virtualSides);
201
+ }
202
+ catch {
203
+ live = true;
204
+ }
205
+ if (live) {
206
+ from = hour;
207
+ break;
208
+ }
209
+ }
210
+ return from;
211
+ }
212
+ async function exportPart(http, out, s, sinceMs, untilMs, progress, refresh, onProgress, onWarning) {
213
+ const id = readId(s, sinceMs, untilMs);
214
+ const dir = join(out, "reads", id);
215
+ mkdirSync(dir, { recursive: true });
216
+ cleanTemporaries(dir);
217
+ const file = join(dir, "read.json");
218
+ const fresh = {
219
+ format: READ_FORMAT,
220
+ ...s,
221
+ sinceMs,
222
+ untilMs,
223
+ coverage: null,
224
+ afterHour: null,
225
+ done: false,
226
+ hours: [],
227
+ hashes: {},
228
+ updatedAt: new Date().toISOString(),
229
+ };
230
+ let meta = refresh ? fresh : (loadMeta(file) ?? fresh);
231
+ if (meta.done) {
232
+ const from = staleFrom(meta, dir);
233
+ if (from !== null) {
234
+ onWarning?.(`${id}: hours from ${new Date(from * HOUR_MS).toISOString()} were open or not sealed when exported, or exported by an older version; downloading them again`);
235
+ const hours = meta.hours.filter((h) => h < from);
236
+ meta = {
237
+ ...meta,
238
+ done: false,
239
+ afterHour: hours.length ? from - 1 : null,
240
+ hours,
241
+ hashes: Object.fromEntries(Object.entries(meta.hashes ?? {}).filter(([h]) => Number(h) < from)),
242
+ };
243
+ }
244
+ }
245
+ const save = () => writeAtomic(file, JSON.stringify(meta, null, 1));
246
+ save();
247
+ while (!meta.done) {
248
+ const query = {
249
+ since_ms: String(sinceMs),
250
+ until_ms: String(untilMs),
251
+ side: s.side,
252
+ allow_partial: "true",
253
+ with_signatures: String(s.withSignatures),
254
+ with_market: String(s.withMarket),
255
+ with_events: String(s.withEvents),
256
+ with_order: String(s.withOrder),
257
+ with_tx: String(!!s.withTx),
258
+ };
259
+ if (meta.afterHour !== null)
260
+ query.after_hour = String(meta.afterHour);
261
+ const answer = await http.raw(`/v1/functions/history/${s.scope}/${encodeURIComponent(s.key)}`, query);
262
+ const { meta: m, file: bytes } = parseFrame(answer.bytes);
263
+ const coverage = m.coverage ?? meta.coverage;
264
+ if (!m.batch) {
265
+ if (typeof m.resumeAfterHour === "number") {
266
+ meta = { ...meta, coverage, afterHour: m.resumeAfterHour };
267
+ save();
268
+ continue;
269
+ }
270
+ const sealed = coverage?.sealedUntilMs ?? 0;
271
+ meta = {
272
+ ...meta,
273
+ coverage,
274
+ done: true,
275
+ updatedAt: new Date().toISOString(),
276
+ };
277
+ save();
278
+ if (sealed < untilMs)
279
+ onWarning?.(`${id}: history after ${new Date(sealed).toISOString()} was not sealed yet; run zyrk export again later to get its final hours`);
280
+ onProgress?.({
281
+ read: id,
282
+ hour: null,
283
+ hours: meta.hours.length,
284
+ done: true,
285
+ bytes: progress.bytes,
286
+ });
287
+ break;
288
+ }
289
+ const { hour } = m.batch;
290
+ let header;
291
+ try {
292
+ header = decompressHour(bytes).header;
293
+ }
294
+ catch (e) {
295
+ throw new ExportError(`Hour ${hour}: ${e.message}`, "decode");
296
+ }
297
+ if (header.hour !== hour)
298
+ throw new ExportError(`Hour ${hour}: inconsistent file`, "decode");
299
+ if (meta.afterHour !== null && hour <= meta.afterHour)
300
+ throw new ExportError(`Hour ${hour} came twice`, "decode");
301
+ writeAtomic(join(dir, `${hour}.zhb`), bytes);
302
+ progress.bytes += bytes.length;
303
+ meta = {
304
+ ...meta,
305
+ coverage,
306
+ afterHour: hour,
307
+ hours: [...meta.hours, hour],
308
+ hashes: { ...meta.hashes, [String(hour)]: sha256(bytes) },
309
+ updatedAt: new Date().toISOString(),
310
+ };
311
+ save();
312
+ onProgress?.({
313
+ read: id,
314
+ hour,
315
+ hours: meta.hours.length,
316
+ done: false,
317
+ bytes: progress.bytes,
318
+ });
319
+ }
320
+ return meta;
321
+ }
322
+ export async function exportHistory(connection, out, read) {
323
+ const http = new Http(connection);
324
+ const s = readSpec(read);
325
+ const progress = { bytes: 0 };
326
+ const reads = [];
327
+ for (const [a, b] of parts(read.sinceMs, read.untilMs)) {
328
+ if (b - a > MAX_WINDOW_MS)
329
+ throw new ExportError("part too long", "bug");
330
+ reads.push(await exportPart(http, out, s, a, b, progress, !!read.refresh, connection.onProgress, connection.onWarning));
331
+ }
332
+ return { reads, requests: http.requests, bytes: progress.bytes };
333
+ }
334
+ const RECORDED = new Set([
335
+ "invalid_window",
336
+ "invalid_parameter",
337
+ "invalid_limit",
338
+ "invalid_amount",
339
+ "invalid_cursor",
340
+ "usd_volume_unavailable",
341
+ "partial_coverage",
342
+ "query_work_limit",
343
+ ]);
344
+ const recordable = (status, body) => {
345
+ const e = body?.error;
346
+ return (status === 400 &&
347
+ typeof e === "object" &&
348
+ e !== null &&
349
+ typeof e.kind === "string" &&
350
+ typeof e.code === "string" &&
351
+ typeof e.message === "string" &&
352
+ RECORDED.has(e.code));
353
+ };
354
+ async function getNew(http, path, query, what) {
355
+ try {
356
+ const { status, body } = await http.get(path, query, recordable);
357
+ if (status !== 400)
358
+ return { ok: body };
359
+ const e = body.error;
360
+ const retry = body.retryAfterMs;
361
+ return {
362
+ code: e.code,
363
+ error: {
364
+ error: { kind: e.kind, code: e.code, message: e.message },
365
+ ...(typeof retry === "number" ? { retryAfterMs: retry } : {}),
366
+ },
367
+ };
368
+ }
369
+ catch (error) {
370
+ if (error instanceof ExportError && error.status === 404)
371
+ throw new ExportError(`This server does not export ${what} yet (it answered 404): it must be upgraded first. Trades (--token, --wallet) export as before.`, "unsupported_server", 404);
372
+ throw error;
373
+ }
374
+ }
375
+ function save(connection, out, [dir, name], answer, content, call) {
376
+ const ok = join(out, dir, name);
377
+ const failed = join(out, "errors", dir, name);
378
+ const [file, other] = "ok" in answer ? [ok, failed] : [failed, ok];
379
+ mkdirSync(dirname(file), { recursive: true });
380
+ writeAtomic(file, JSON.stringify(content));
381
+ rmSync(other, { force: true });
382
+ if (!("ok" in answer))
383
+ connection.onWarning?.(`${call} answered ${answer.code}: saved as its answer; the same call in a local run throws this error, as a hosted call does now`);
384
+ }
385
+ export async function exportTotals(connection, out, mint, quote = null) {
386
+ const http = new Http(connection);
387
+ const key = encodeURIComponent(mint);
388
+ const body = await getNew(http, `/v1/functions/totals/${key}`, quote ? { quote } : {}, "totals (--totals)");
389
+ const info = await getNew(http, `/v1/functions/info/${key}`, {}, "totals (--totals)");
390
+ const content = (a) => ("ok" in a ? a.ok : a.error);
391
+ const totals = content(body);
392
+ save(connection, out, ["totals", totalsName(mint, quote)], body, totals, "tokenTotals");
393
+ save(connection, out, ["info", infoName(mint)], info, content(info), "tokenInfo");
394
+ return totals;
395
+ }
396
+ export async function exportVolumeSearch(connection, out, params) {
397
+ const http = new Http(connection);
398
+ mkdirSync(join(out, "volume"), { recursive: true });
399
+ let p = { ...params };
400
+ for (let pages = 1;; pages++) {
401
+ const flat = Object.entries(p)
402
+ .filter(([, v]) => v !== undefined && v !== null)
403
+ .map(([k, v]) => [k, String(v)]);
404
+ const answer = await getNew(http, "/v1/functions/volume/search", Object.fromEntries(flat), "volume searches (--volume-search)");
405
+ save(connection, out, ["volume", `${volumeName(flat)}.json`], answer, "ok" in answer
406
+ ? { params: flat, page: answer.ok }
407
+ : { params: flat, ...answer.error }, `volumeSearch (page ${pages})`);
408
+ if (!("ok" in answer))
409
+ return pages;
410
+ const page = answer.ok;
411
+ const next = page?.nextCursor;
412
+ if (next === null || next === undefined)
413
+ return pages;
414
+ if (typeof next !== "string" || next === p.cursor)
415
+ throw new ExportError("Invalid next cursor", "decode");
416
+ const w = page
417
+ .window;
418
+ if (w && typeof w === "object" && "since" in w)
419
+ p = { ...p, since: w.since, until: w.until };
420
+ p = { ...p, cursor: next };
421
+ }
422
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * The only globals of a hosted program besides the ECMAScript built-ins.
3
+ * `zyrk build` type-checks against them; in an editor, add
4
+ * `/// <reference types="@johannc-dev/zyrk/globals" />` with `"types": []`
5
+ * and `"lib": ["ES2023"]` in the program's tsconfig.
6
+ *
7
+ * Absent on purpose: `fetch` and every network API, timers, Node APIs, DOM.
8
+ */
9
+ export {};
10
+
11
+ declare global {
12
+ /** Each call becomes a `ctx.log` entry of the execution. */
13
+ interface Console {
14
+ log(...values: unknown[]): void;
15
+ info(...values: unknown[]): void;
16
+ warn(...values: unknown[]): void;
17
+ error(...values: unknown[]): void;
18
+ debug(...values: unknown[]): void;
19
+ }
20
+ var console: Console;
21
+
22
+ interface TextEncoderEncodeIntoResult {
23
+ read: number;
24
+ written: number;
25
+ }
26
+ interface TextEncoder {
27
+ readonly encoding: "utf-8";
28
+ encode(input?: string): Uint8Array;
29
+ encodeInto(
30
+ input: string,
31
+ destination: Uint8Array,
32
+ ): TextEncoderEncodeIntoResult;
33
+ }
34
+ var TextEncoder: { prototype: TextEncoder; new (): TextEncoder };
35
+
36
+ interface TextDecoder {
37
+ readonly encoding: string;
38
+ readonly fatal: boolean;
39
+ readonly ignoreBOM: boolean;
40
+ decode(
41
+ input?: ArrayBufferView | ArrayBuffer,
42
+ options?: { stream?: boolean },
43
+ ): string;
44
+ }
45
+ var TextDecoder: {
46
+ prototype: TextDecoder;
47
+ new (
48
+ label?: string,
49
+ options?: { fatal?: boolean; ignoreBOM?: boolean },
50
+ ): TextDecoder;
51
+ };
52
+
53
+ function structuredClone<T>(value: T): T;
54
+
55
+ interface Crypto {
56
+ getRandomValues<T extends ArrayBufferView | null>(array: T): T;
57
+ }
58
+ var crypto: Crypto;
59
+ }
@@ -0,0 +1,59 @@
1
+ import { type HourBatch } from "./sdk.js";
2
+ export declare const HOUR_MAGIC = "ZHB1";
3
+ export type ColumnType = "u8" | "u16" | "u32" | "i32" | "u64" | "f64";
4
+ export interface HourHeader {
5
+ readonly hour: number;
6
+ readonly source: "sealed" | "live";
7
+ readonly complete: boolean;
8
+ readonly skipped: number;
9
+ readonly length: number;
10
+ readonly addresses: readonly string[];
11
+ readonly signatures?: readonly (string | null)[];
12
+ /** Exports: timestamp of each operation counted in `skipped` (`null`: unknown). */
13
+ readonly skippedTimes?: readonly (number | null)[];
14
+ readonly market: boolean;
15
+ /** Market flags with one bit per virtual side; absent from older files. */
16
+ readonly virtualSides?: boolean;
17
+ readonly order: boolean;
18
+ /** Absent from files written before transaction ids. */
19
+ readonly tx?: boolean;
20
+ readonly columns: readonly (readonly [string, ColumnType, number])[];
21
+ }
22
+ /** Typed array constructors of the realm the batch is built for (a `vm` context, or Node's own). */
23
+ export interface Realm {
24
+ readonly Uint8Array: Uint8ArrayConstructor;
25
+ readonly Uint16Array: Uint16ArrayConstructor;
26
+ readonly Uint32Array: Uint32ArrayConstructor;
27
+ readonly Int32Array: Int32ArrayConstructor;
28
+ readonly BigUint64Array: BigUint64ArrayConstructor;
29
+ readonly Float64Array: Float64ArrayConstructor;
30
+ readonly Array: ArrayConstructor;
31
+ readonly Object: ObjectConstructor;
32
+ }
33
+ export declare const nodeRealm: Realm;
34
+ /** Column names and types, in the order of the sandbox's batch object. */
35
+ export declare const COLUMNS: readonly (readonly [string, ColumnType])[];
36
+ export declare const MARKET_COLUMNS: readonly (readonly [string, ColumnType])[];
37
+ export declare const ORDER_COLUMNS: readonly (readonly [string, ColumnType])[];
38
+ export declare const TX_COLUMNS: readonly (readonly [string, ColumnType])[];
39
+ export declare class HourFileError extends Error {
40
+ readonly name = "HourFileError";
41
+ }
42
+ /** A decoded `ZHB1` buffer: its header and the raw bytes behind its columns. */
43
+ export interface RawHour {
44
+ readonly header: HourHeader;
45
+ readonly bytes: Uint8Array;
46
+ }
47
+ export declare function decompressHour(file: Uint8Array): RawHour;
48
+ /** Checks the header and every column, then the address indexes (see {@link checkIndexes}). */
49
+ export declare function parseHour(bytes: Uint8Array): RawHour;
50
+ /**
51
+ * The batch of a raw hour, every column in its own buffer (as in the
52
+ * sandbox), arrays of `realm`. Property order is the sandbox's.
53
+ */
54
+ export declare function batchOf(raw: RawHour, realm?: Realm): HourBatch;
55
+ /** The `ZHB1` buffer of a batch (what the server writes; used by tests and local re-cuts). */
56
+ export declare function encodeHour(batch: HourBatch): Uint8Array;
57
+ /** zstd with the frame checksum, as the server writes exported hours. */
58
+ export declare function compress(raw: Uint8Array): Uint8Array;
59
+ export declare function compressHour(batch: HourBatch): Uint8Array;