@johannc-dev/zyrk 0.2.0 → 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.
- package/README.md +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +4 -4
- package/dist/client.d.ts +6 -2
- package/dist/client.js +510 -17
- package/dist/errors.d.ts +9 -1
- package/dist/errors.js +6 -1
- package/dist/export.d.ts +16 -7
- package/dist/export.js +102 -18
- package/dist/hourfile.d.ts +3 -1
- package/dist/hourfile.js +7 -0
- package/dist/limits.d.ts +1 -1
- package/dist/local-data.d.ts +13 -4
- package/dist/local-data.js +29 -1
- package/dist/local-replay.d.ts +5 -5
- package/dist/local-replay.js +4 -3
- package/dist/local-storage.d.ts +2 -2
- package/dist/local.d.ts +2 -2
- package/dist/local.js +46 -32
- package/dist/sandbox-bootstrap.js +171 -9
- package/dist/sdk.d.ts +51 -28
- package/dist/sdk.js +2 -0
- package/dist/stream.d.ts +52 -4
- package/dist/stream.js +111 -13
- package/package.json +3 -3
package/dist/export.d.ts
CHANGED
|
@@ -7,14 +7,17 @@ export interface ExportConnection {
|
|
|
7
7
|
readonly apiKey: string;
|
|
8
8
|
/** At most this many requests per minute (default: as fast as the server allows). */
|
|
9
9
|
readonly rpm?: number;
|
|
10
|
-
/** Network
|
|
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 a
|
|
12
|
+
/** Longest wait accepted on 429s in a row before stopping (default 5 minutes): a daily quota stops the export. */
|
|
13
13
|
readonly maxWaitMs?: number;
|
|
14
14
|
readonly fetch?: typeof fetch;
|
|
15
15
|
readonly signal?: AbortSignal;
|
|
16
16
|
readonly onProgress?: (progress: ExportProgress) => void;
|
|
17
|
-
/**
|
|
17
|
+
/**
|
|
18
|
+
* Hours that may still change (open, not sealed), files downloaded again,
|
|
19
|
+
* errors saved as the answer of a call.
|
|
20
|
+
*/
|
|
18
21
|
readonly onWarning?: (message: string) => void;
|
|
19
22
|
}
|
|
20
23
|
export interface ExportProgress {
|
|
@@ -52,7 +55,7 @@ export interface ExportSummary {
|
|
|
52
55
|
}
|
|
53
56
|
/** `[since, until)` in parts of at most 30 days, ending on UTC day boundaries. */
|
|
54
57
|
export declare function parts(sinceMs: number, untilMs: number): [number, number][];
|
|
55
|
-
/** Meta of an export answer
|
|
58
|
+
/** Meta of an export answer. */
|
|
56
59
|
interface HourMeta {
|
|
57
60
|
readonly format?: string;
|
|
58
61
|
readonly coverage?: ReadMeta["coverage"];
|
|
@@ -76,12 +79,18 @@ export declare function parseFrame(bytes: Uint8Array): {
|
|
|
76
79
|
export declare function staleFrom(meta: ReadMeta, dir: string): number | null;
|
|
77
80
|
/** Exports one read (cut into parts of at most 30 days). Resumes an interrupted export. */
|
|
78
81
|
export declare function exportHistory(connection: ExportConnection, out: string, read: ExportRead): Promise<ExportSummary>;
|
|
79
|
-
/**
|
|
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
|
+
*/
|
|
80
87
|
export declare function exportTotals(connection: ExportConnection, out: string, mint: string, quote?: string | null): Promise<Json>;
|
|
81
88
|
/**
|
|
82
|
-
* Saves every page of
|
|
89
|
+
* Saves every page of a volume search for `params`, following the
|
|
83
90
|
* cursor exactly as `ctx.data.volumeSearch` does, so that the same call in
|
|
84
|
-
* a local run finds each page.
|
|
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.
|
|
85
94
|
*/
|
|
86
95
|
export declare function exportVolumeSearch(connection: ExportConnection, out: string, params: VolumeSearchParams): Promise<number>;
|
|
87
96
|
export {};
|
package/dist/export.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
3
|
import { decompressHour } from "./hourfile.js";
|
|
4
|
-
import { HOUR_MS, READ_FORMAT, 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, } 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;
|
|
@@ -40,6 +40,8 @@ const writeAtomic = (file, data) => {
|
|
|
40
40
|
writeFileSync(tmp, data);
|
|
41
41
|
renameSync(tmp, file);
|
|
42
42
|
};
|
|
43
|
+
const MAX_BUSY_ATTEMPTS = 12;
|
|
44
|
+
const MAX_SERVER_ERRORS = 1;
|
|
43
45
|
class Http {
|
|
44
46
|
c;
|
|
45
47
|
requests = 0;
|
|
@@ -47,12 +49,15 @@ class Http {
|
|
|
47
49
|
constructor(c) {
|
|
48
50
|
this.c = c;
|
|
49
51
|
}
|
|
50
|
-
async raw(path, query) {
|
|
52
|
+
async raw(path, query, keep) {
|
|
51
53
|
const f = this.c.fetch ?? fetch;
|
|
52
54
|
const url = new URL(this.c.url.replace(/\/+$/, "") + path);
|
|
53
55
|
for (const [k, v] of Object.entries(query))
|
|
54
56
|
url.searchParams.set(k, v);
|
|
55
57
|
let failures = 0;
|
|
58
|
+
let serverErrors = 0;
|
|
59
|
+
let busy = 0;
|
|
60
|
+
let waited = 0;
|
|
56
61
|
for (;;) {
|
|
57
62
|
if (this.c.rpm) {
|
|
58
63
|
const gap = 60_000 / this.c.rpm - (Date.now() - this.#last);
|
|
@@ -88,6 +93,8 @@ class Http {
|
|
|
88
93
|
catch {
|
|
89
94
|
body = null;
|
|
90
95
|
}
|
|
96
|
+
if (keep?.(response.status, body))
|
|
97
|
+
return { status: response.status, contentType, bytes };
|
|
91
98
|
const err = body?.error;
|
|
92
99
|
const code = typeof err === "string"
|
|
93
100
|
? err
|
|
@@ -105,12 +112,26 @@ class Http {
|
|
|
105
112
|
? header * 1000
|
|
106
113
|
: undefined;
|
|
107
114
|
if (response.status === 429) {
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
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;
|
|
111
123
|
await sleep(wait, this.c.signal);
|
|
112
124
|
continue;
|
|
113
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
|
+
}
|
|
114
135
|
if (response.status >= 500 && code !== "history_timeout") {
|
|
115
136
|
if (++failures > (this.c.maxRetries ?? 8))
|
|
116
137
|
throw new ExportError(`${response.status} ${code}: ${message}`, code, response.status);
|
|
@@ -120,11 +141,14 @@ class Http {
|
|
|
120
141
|
throw new ExportError(`${response.status} ${code}: ${message}`, code, response.status);
|
|
121
142
|
}
|
|
122
143
|
}
|
|
123
|
-
async get(path, query) {
|
|
124
|
-
const answer = await this.raw(path, query);
|
|
144
|
+
async get(path, query, keep) {
|
|
145
|
+
const answer = await this.raw(path, query, keep);
|
|
125
146
|
const text = new TextDecoder().decode(answer.bytes);
|
|
126
147
|
try {
|
|
127
|
-
return
|
|
148
|
+
return {
|
|
149
|
+
status: answer.status,
|
|
150
|
+
body: text ? JSON.parse(text) : null,
|
|
151
|
+
};
|
|
128
152
|
}
|
|
129
153
|
catch {
|
|
130
154
|
throw new ExportError(`${path}: the answer is not JSON`, "decode");
|
|
@@ -171,9 +195,9 @@ export function staleFrom(meta, dir) {
|
|
|
171
195
|
break;
|
|
172
196
|
let live = false;
|
|
173
197
|
try {
|
|
198
|
+
const { header } = decompressHour(readFileSync(join(dir, `${hour}.zhb`)));
|
|
174
199
|
live =
|
|
175
|
-
|
|
176
|
-
"live";
|
|
200
|
+
header.source === "live" || (header.market && !header.virtualSides);
|
|
177
201
|
}
|
|
178
202
|
catch {
|
|
179
203
|
live = true;
|
|
@@ -207,7 +231,7 @@ async function exportPart(http, out, s, sinceMs, untilMs, progress, refresh, onP
|
|
|
207
231
|
if (meta.done) {
|
|
208
232
|
const from = staleFrom(meta, dir);
|
|
209
233
|
if (from !== null) {
|
|
210
|
-
onWarning?.(`${id}: hours from ${new Date(from * HOUR_MS).toISOString()} were open or not sealed when exported; downloading them again`);
|
|
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`);
|
|
211
235
|
const hours = meta.hours.filter((h) => h < from);
|
|
212
236
|
meta = {
|
|
213
237
|
...meta,
|
|
@@ -307,12 +331,67 @@ export async function exportHistory(connection, out, read) {
|
|
|
307
331
|
}
|
|
308
332
|
return { reads, requests: http.requests, bytes: progress.bytes };
|
|
309
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
|
+
}
|
|
310
385
|
export async function exportTotals(connection, out, mint, quote = null) {
|
|
311
386
|
const http = new Http(connection);
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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;
|
|
316
395
|
}
|
|
317
396
|
export async function exportVolumeSearch(connection, out, params) {
|
|
318
397
|
const http = new Http(connection);
|
|
@@ -322,8 +401,13 @@ export async function exportVolumeSearch(connection, out, params) {
|
|
|
322
401
|
const flat = Object.entries(p)
|
|
323
402
|
.filter(([, v]) => v !== undefined && v !== null)
|
|
324
403
|
.map(([k, v]) => [k, String(v)]);
|
|
325
|
-
const
|
|
326
|
-
|
|
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;
|
|
327
411
|
const next = page?.nextCursor;
|
|
328
412
|
if (next === null || next === undefined)
|
|
329
413
|
return pages;
|
package/dist/hourfile.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type HourBatch } from "./sdk.js";
|
|
2
2
|
export declare const HOUR_MAGIC = "ZHB1";
|
|
3
3
|
export type ColumnType = "u8" | "u16" | "u32" | "i32" | "u64" | "f64";
|
|
4
4
|
export interface HourHeader {
|
|
@@ -12,6 +12,8 @@ export interface HourHeader {
|
|
|
12
12
|
/** Exports: timestamp of each operation counted in `skipped` (`null`: unknown). */
|
|
13
13
|
readonly skippedTimes?: readonly (number | null)[];
|
|
14
14
|
readonly market: boolean;
|
|
15
|
+
/** Market flags with one bit per virtual side; absent from older files. */
|
|
16
|
+
readonly virtualSides?: boolean;
|
|
15
17
|
readonly order: boolean;
|
|
16
18
|
/** Absent from files written before transaction ids. */
|
|
17
19
|
readonly tx?: boolean;
|
package/dist/hourfile.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { constants, zstdCompressSync, zstdDecompressSync } from "node:zlib";
|
|
2
|
+
import { MarketFlag } from "./sdk.js";
|
|
2
3
|
export const HOUR_MAGIC = "ZHB1";
|
|
3
4
|
export const nodeRealm = {
|
|
4
5
|
Uint8Array,
|
|
@@ -237,6 +238,12 @@ export function batchOf(raw, realm = nodeRealm) {
|
|
|
237
238
|
const m = new realm.Object();
|
|
238
239
|
for (const [name, type] of MARKET_COLUMNS)
|
|
239
240
|
m[name] = column(`market.${name}`, type);
|
|
241
|
+
const flags = m.flags;
|
|
242
|
+
if (!header.virtualSides)
|
|
243
|
+
for (let i = 0; i < flags.length; i++)
|
|
244
|
+
if ((flags[i] & MarketFlag.virtualReserves) !== 0)
|
|
245
|
+
flags[i] |=
|
|
246
|
+
MarketFlag.virtualTokenReserve | MarketFlag.virtualQuoteReserve;
|
|
240
247
|
batch.market = m;
|
|
241
248
|
}
|
|
242
249
|
if (header.order) {
|
package/dist/limits.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Limits of
|
|
2
|
+
* Limits of hosted functions, mirrored from the server defaults. The server is the
|
|
3
3
|
* authority; these constants let tools and programs plan around them.
|
|
4
4
|
*
|
|
5
5
|
* No dependency: safe to import from guest code.
|
package/dist/local-data.d.ts
CHANGED
|
@@ -69,7 +69,7 @@ export declare class ReadFailed extends Error {
|
|
|
69
69
|
export declare const sha256: (bytes: Uint8Array) => string;
|
|
70
70
|
export declare function windowComplete(c: Coverage, since: number, until: number): boolean;
|
|
71
71
|
export declare function hourComplete(c: Coverage, hour: number, since: number, until: number): boolean;
|
|
72
|
-
/**
|
|
72
|
+
/** What a batch costs against `readBytes`, as the service counts it. */
|
|
73
73
|
export declare function batchBytes(h: HourHeader): number;
|
|
74
74
|
export interface HourRead {
|
|
75
75
|
readonly batch: HourBatch;
|
|
@@ -93,14 +93,23 @@ export declare class LocalData {
|
|
|
93
93
|
find(s: ReadSpec, since: number, until: number): ReadMeta | undefined;
|
|
94
94
|
/**
|
|
95
95
|
* The next batch of a read after `afterHour` (`null`: none left), as
|
|
96
|
-
*
|
|
96
|
+
* the service's reader answers it. Throws {@link ReadFailed}.
|
|
97
97
|
*/
|
|
98
98
|
readHour(s: ReadSpec, since: number, until: number, allowPartial: boolean, afterHour: number | null, realm?: Realm): HourRead | null;
|
|
99
|
-
/**
|
|
99
|
+
/** Totals of a mint saved by the export, or `undefined`. */
|
|
100
100
|
totals(mint: string, quote: string | null): Json | undefined;
|
|
101
|
-
/**
|
|
101
|
+
/** Info of a mint saved by the export (`--totals`), or `undefined`. */
|
|
102
|
+
info(mint: string): Json | undefined;
|
|
103
|
+
/** The error `tokenTotals` answered at export, or `undefined`. */
|
|
104
|
+
totalsFailure(mint: string, quote: string | null): ReadFailure | undefined;
|
|
105
|
+
/** The error `tokenInfo` answered at export, or `undefined`. */
|
|
106
|
+
infoFailure(mint: string): ReadFailure | undefined;
|
|
107
|
+
/** The error a volume search page answered at export, or `undefined`. */
|
|
108
|
+
volumeFailure(params: readonly (readonly [string, string])[]): ReadFailure | undefined;
|
|
109
|
+
/** A volume search page saved by the export for exactly these parameters. */
|
|
102
110
|
volumePage(params: readonly (readonly [string, string])[]): Json | undefined;
|
|
103
111
|
}
|
|
104
112
|
export declare const totalsName: (mint: string, quote: string | null) => string;
|
|
113
|
+
export declare const infoName: (mint: string) => string;
|
|
105
114
|
/** Parameters as the sandbox passes them (`[key, string value]`), sorted. */
|
|
106
115
|
export declare function volumeName(params: readonly (readonly [string, string])[]): string;
|
package/dist/local-data.js
CHANGED
|
@@ -315,7 +315,8 @@ export class LocalData {
|
|
|
315
315
|
batch: batchOf(raw, realm),
|
|
316
316
|
bytes: batchBytes(raw.header),
|
|
317
317
|
recut: recuts,
|
|
318
|
-
stale: stale(raw.header.source, hour)
|
|
318
|
+
stale: stale(raw.header.source, hour) ||
|
|
319
|
+
(raw.header.market && !raw.header.virtualSides),
|
|
319
320
|
};
|
|
320
321
|
}
|
|
321
322
|
return null;
|
|
@@ -330,15 +331,42 @@ export class LocalData {
|
|
|
330
331
|
throw corrupt(file, e, again);
|
|
331
332
|
}
|
|
332
333
|
}
|
|
334
|
+
#failure(file, again) {
|
|
335
|
+
const saved = this.#json(join(this.dir, "errors", file), again);
|
|
336
|
+
if (saved === undefined)
|
|
337
|
+
return undefined;
|
|
338
|
+
const { code, message } = saved?.error ?? {};
|
|
339
|
+
if (typeof code !== "string" || typeof message !== "string")
|
|
340
|
+
throw corrupt(join(this.dir, "errors", file), new Error("not an error answer"), again);
|
|
341
|
+
const retry = saved.retryAfterMs;
|
|
342
|
+
return {
|
|
343
|
+
code,
|
|
344
|
+
message,
|
|
345
|
+
...(typeof retry === "number" ? { retryAfterMs: retry } : {}),
|
|
346
|
+
};
|
|
347
|
+
}
|
|
333
348
|
totals(mint, quote) {
|
|
334
349
|
return this.#json(join(this.dir, "totals", totalsName(mint, quote)), `zyrk export --totals ${mint}${quote ? ` --quote ${quote}` : ""}`);
|
|
335
350
|
}
|
|
351
|
+
info(mint) {
|
|
352
|
+
return this.#json(join(this.dir, "info", infoName(mint)), `zyrk export --totals ${mint}`);
|
|
353
|
+
}
|
|
354
|
+
totalsFailure(mint, quote) {
|
|
355
|
+
return this.#failure(join("totals", totalsName(mint, quote)), `zyrk export --totals ${mint}${quote ? ` --quote ${quote}` : ""}`);
|
|
356
|
+
}
|
|
357
|
+
infoFailure(mint) {
|
|
358
|
+
return this.#failure(join("info", infoName(mint)), `zyrk export --totals ${mint}`);
|
|
359
|
+
}
|
|
360
|
+
volumeFailure(params) {
|
|
361
|
+
return this.#failure(join("volume", `${volumeName(params)}.json`), "zyrk export --volume-search '<params>'");
|
|
362
|
+
}
|
|
336
363
|
volumePage(params) {
|
|
337
364
|
const saved = this.#json(join(this.dir, "volume", `${volumeName(params)}.json`), "zyrk export --volume-search '<params>'");
|
|
338
365
|
return saved?.page;
|
|
339
366
|
}
|
|
340
367
|
}
|
|
341
368
|
export const totalsName = (mint, quote) => quote ? `${mint}~${quote}.json` : `${mint}.json`;
|
|
369
|
+
export const infoName = (mint) => `${mint}.json`;
|
|
342
370
|
export function volumeName(params) {
|
|
343
371
|
const canonical = JSON.stringify([...params].sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0)));
|
|
344
372
|
return createHash("sha256").update(canonical).digest("hex").slice(0, 32);
|
package/dist/local-replay.d.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { type LocalData, type ReadMeta } from "./local-data.js";
|
|
2
2
|
import { type HourBatch, type Subscription } from "./sdk.js";
|
|
3
|
-
/**
|
|
3
|
+
/** The first 30 s after coverage starts are not trusted (as the service). */
|
|
4
4
|
export declare const LAG_MS = 30000;
|
|
5
5
|
/** Events per delivery, as `delivery_batch` of the service. */
|
|
6
6
|
export declare const DELIVERY_BATCH = 32;
|
|
7
|
-
/** Largest `batchSize` of a replay
|
|
7
|
+
/** Largest `batchSize` of a replay of the service. */
|
|
8
8
|
export declare const REPLAY_MAX_BATCH = 1000;
|
|
9
|
-
/** History items per source page of the service
|
|
9
|
+
/** History items per source page of the service's replays. */
|
|
10
10
|
export declare const SOURCE_PAGE = 500;
|
|
11
11
|
/**
|
|
12
|
-
* Fields of the live operation JSON
|
|
12
|
+
* Fields of the live operation JSON that exported
|
|
13
13
|
* columns do not hold: absent from replayed operations.
|
|
14
14
|
*/
|
|
15
15
|
export declare const MISSING_OPERATION_FIELDS: readonly ["prices (provider strings: read batch.price… instead)", "usdValue", "finality", "provenance", "market (the provider's raw object)"];
|
|
@@ -26,7 +26,7 @@ export interface ReplayProgress {
|
|
|
26
26
|
gaps: number;
|
|
27
27
|
orderFallbacks: number;
|
|
28
28
|
}
|
|
29
|
-
/** Decimal string of a raw amount
|
|
29
|
+
/** Decimal string of a raw amount, as the live JSON writes it. */
|
|
30
30
|
export declare function formatRaw(raw: bigint, decimals: number): string;
|
|
31
31
|
/** Row `i` as the live publication shapes it (without {@link MISSING_OPERATION_FIELDS}). */
|
|
32
32
|
export declare function operationOf(b: HourBatch, i: number): Record<string, unknown>;
|
package/dist/local-replay.js
CHANGED
|
@@ -82,7 +82,8 @@ function poolEventOf(b, i, kind) {
|
|
|
82
82
|
const m = b.market;
|
|
83
83
|
const flags = m.flags[i];
|
|
84
84
|
const reserves = (flags & MarketFlag.reserves) !== 0;
|
|
85
|
-
const
|
|
85
|
+
const vToken = (flags & MarketFlag.virtualTokenReserve) !== 0;
|
|
86
|
+
const vQuote = (flags & MarketFlag.virtualQuoteReserve) !== 0;
|
|
86
87
|
const op = operationOf(b, i);
|
|
87
88
|
return {
|
|
88
89
|
...op,
|
|
@@ -93,10 +94,10 @@ function poolEventOf(b, i, kind) {
|
|
|
93
94
|
market: {
|
|
94
95
|
tokenReserveRaw: reserves ? m.tokenReserve[i].toString() : null,
|
|
95
96
|
quoteReserveRaw: reserves ? m.quoteReserve[i].toString() : null,
|
|
96
|
-
virtualTokenReserveRaw:
|
|
97
|
+
virtualTokenReserveRaw: vToken
|
|
97
98
|
? m.virtualTokenReserve[i].toString()
|
|
98
99
|
: null,
|
|
99
|
-
virtualQuoteReserveRaw:
|
|
100
|
+
virtualQuoteReserveRaw: vQuote
|
|
100
101
|
? m.virtualQuoteReserve[i].toString()
|
|
101
102
|
: null,
|
|
102
103
|
signer: addressAt(b, m.signer[i]),
|
package/dist/local-storage.d.ts
CHANGED
|
@@ -9,9 +9,9 @@ export declare const localStorageLimits: {
|
|
|
9
9
|
readonly maxUploads: 4;
|
|
10
10
|
/** Bytes read plus written by one execution (`io_limit`). */
|
|
11
11
|
readonly ioBytes: number;
|
|
12
|
-
/** Quota cost floor of one object
|
|
12
|
+
/** Quota cost floor of one object. */
|
|
13
13
|
readonly minObjectBytes: 4096;
|
|
14
|
-
/** Objects per area
|
|
14
|
+
/** Objects per area. */
|
|
15
15
|
readonly maxObjects: 100000;
|
|
16
16
|
};
|
|
17
17
|
export interface AreaConfig {
|
package/dist/local.d.ts
CHANGED
|
@@ -199,9 +199,9 @@ export interface KeepStorage {
|
|
|
199
199
|
readonly from?: "short" | "long" | "durable";
|
|
200
200
|
readonly keys?: readonly string[];
|
|
201
201
|
}
|
|
202
|
-
/**
|
|
202
|
+
/** Longest replay of the service (30 min). */
|
|
203
203
|
export declare const REPLAY_MAX_DURATION_MS: number;
|
|
204
|
-
/**
|
|
204
|
+
/** Unread feed of one replay on the service (16 MiB). */
|
|
205
205
|
export declare const REPLAY_FEED_BYTES: number;
|
|
206
206
|
/** What {@link LocalRunner.replay} answers (the service's replay record, locally). */
|
|
207
207
|
export interface LocalReplay {
|
package/dist/local.js
CHANGED
|
@@ -6,10 +6,21 @@ 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, 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));
|
|
13
|
+
let batchReadsSaid = false;
|
|
14
|
+
function batchReadsFromEnv() {
|
|
15
|
+
const raw = process.env.FUNRUN_BATCH_READS;
|
|
16
|
+
const v = (raw ?? "").trim();
|
|
17
|
+
const on = v === "on" || v === "1" || v === "true";
|
|
18
|
+
if (raw !== undefined && !batchReadsSaid) {
|
|
19
|
+
batchReadsSaid = true;
|
|
20
|
+
process.stderr.write(`zyrk local runner: FUNRUN_BATCH_READS=${JSON.stringify(raw)}: batched history reads ${on ? "on" : "off"} (hosted functions batch only when the service has the same setting; off there by default)\n`);
|
|
21
|
+
}
|
|
22
|
+
return on;
|
|
23
|
+
}
|
|
13
24
|
let bootstrapText;
|
|
14
25
|
export function sandboxBootstrap() {
|
|
15
26
|
return (bootstrapText ??= readFileSync(join(here, "sandbox-bootstrap.js"), "utf8"));
|
|
@@ -60,6 +71,13 @@ const WEBHOOK_PAYLOAD_BYTES = 16 * 1024;
|
|
|
60
71
|
const WEBHOOK_OVERHEAD_BYTES = 256;
|
|
61
72
|
const WEBHOOK_QUEUE_LIMIT = 1000;
|
|
62
73
|
const MAX_MESSAGE_BYTES = 2048;
|
|
74
|
+
const openWhenExported = (v) => {
|
|
75
|
+
const c = v?.coverage;
|
|
76
|
+
return (typeof c === "object" &&
|
|
77
|
+
c !== null &&
|
|
78
|
+
(c.unsealed === true ||
|
|
79
|
+
(c.liveUntilMs !== null && c.liveUntilMs !== undefined)));
|
|
80
|
+
};
|
|
63
81
|
const jsonBytes = (v) => Buffer.byteLength(JSON.stringify(v) ?? "null");
|
|
64
82
|
const truncate = (m) => Buffer.byteLength(m) <= MAX_MESSAGE_BYTES
|
|
65
83
|
? m
|
|
@@ -459,8 +477,31 @@ export class LocalRunner {
|
|
|
459
477
|
throw sdkError(program("local_data_missing", "No data directory: export history with zyrk export, then pass --data (or { data })."));
|
|
460
478
|
return this.data;
|
|
461
479
|
};
|
|
480
|
+
const saved = (answer, failure, missing) => {
|
|
481
|
+
let value;
|
|
482
|
+
let failed;
|
|
483
|
+
try {
|
|
484
|
+
value = answer();
|
|
485
|
+
failed = value === undefined ? failure() : undefined;
|
|
486
|
+
}
|
|
487
|
+
catch (error) {
|
|
488
|
+
if (error instanceof ReadFailed)
|
|
489
|
+
throw sdkError(fromRead(error.failure));
|
|
490
|
+
throw error;
|
|
491
|
+
}
|
|
492
|
+
if (failed)
|
|
493
|
+
throw sdkError(fromRead(failed));
|
|
494
|
+
if (value === undefined)
|
|
495
|
+
throw sdkError(program("local_data_missing", missing));
|
|
496
|
+
if (openWhenExported(value))
|
|
497
|
+
this.#warn("Totals or volume search pages were exported while an hour was still open: the service may now answer otherwise. Run zyrk export again (--totals, --volume-search) to download them again.");
|
|
498
|
+
charge(jsonBytes(value));
|
|
499
|
+
return guestJson(value);
|
|
500
|
+
};
|
|
462
501
|
return {
|
|
463
502
|
maxDepth: this.limits.account.maxDepth,
|
|
503
|
+
batchReads: batchReadsFromEnv(),
|
|
504
|
+
hint: () => undefined,
|
|
464
505
|
fail: (code, message) => sdkError(new HostError(kindOfSdkCode(String(code)), String(code), String(message))),
|
|
465
506
|
op: (text, awaited) => this.#op(e, text, !!awaited, realm, mode, sdkError, guestJson, sample),
|
|
466
507
|
read: (kind, key, since, until, side, allowPartial, withSignatures, after, withMarket, withEvents, withOrder, withTx) => {
|
|
@@ -505,7 +546,7 @@ export class LocalRunner {
|
|
|
505
546
|
if (hour === null)
|
|
506
547
|
return null;
|
|
507
548
|
if (hour.stale)
|
|
508
|
-
this.#warn("Some hours were open or not sealed when they were exported: the service may now answer otherwise. Run zyrk export again to download them again.");
|
|
549
|
+
this.#warn("Some hours were open or not sealed when they were exported, or were exported by an older version: the service may now answer otherwise. Run zyrk export again to download them again.");
|
|
509
550
|
if (hour.recut)
|
|
510
551
|
this.#warn("A read used a narrower window than its export: its edge hours were cut locally. Values are the service's; the address indexes and market.flags (chainBreak, chainUnchecked) of a cut hour may differ. Export the exact window for byte-identical batches.");
|
|
511
552
|
charge(hour.bytes);
|
|
@@ -517,47 +558,20 @@ export class LocalRunner {
|
|
|
517
558
|
if (typeof mint !== "string" ||
|
|
518
559
|
(quote !== null && typeof quote !== "string"))
|
|
519
560
|
throw sdkError(program("invalid_argument", "Expected a mint address"));
|
|
520
|
-
|
|
521
|
-
if (value === undefined)
|
|
522
|
-
throw sdkError(program("local_data_missing", `No exported totals for ${mint}: zyrk export --totals ${mint}${quote ? ` --quote ${quote}` : ""}`));
|
|
523
|
-
charge(jsonBytes(value));
|
|
524
|
-
return guestJson(value);
|
|
561
|
+
return saved(() => data().totals(mint, quote), () => data().totalsFailure(mint, quote), `No exported totals for ${mint}: zyrk export --totals ${mint}${quote ? ` --quote ${quote}` : ""}`);
|
|
525
562
|
},
|
|
526
563
|
info: (mint) => {
|
|
527
564
|
readAllowed();
|
|
528
565
|
e.usage.readCalls++;
|
|
529
566
|
if (typeof mint !== "string")
|
|
530
567
|
throw sdkError(program("invalid_argument", "Expected a mint address"));
|
|
531
|
-
|
|
532
|
-
if (t === undefined)
|
|
533
|
-
throw sdkError(program("local_data_missing", `No exported totals for ${mint}: zyrk export --totals ${mint}`));
|
|
534
|
-
const creation = (t.creation ?? null);
|
|
535
|
-
const metadata = (t.metadata ?? null);
|
|
536
|
-
const created = creation !== null &&
|
|
537
|
-
typeof creation === "object" &&
|
|
538
|
-
!Array.isArray(creation) &&
|
|
539
|
-
typeof creation.timestamp === "number"
|
|
540
|
-
? creation.timestamp
|
|
541
|
-
: (t.createdMs ?? null);
|
|
542
|
-
const value = {
|
|
543
|
-
mint,
|
|
544
|
-
known: creation !== null || metadata !== null || created !== null,
|
|
545
|
-
createdMs: created,
|
|
546
|
-
creation,
|
|
547
|
-
metadata,
|
|
548
|
-
};
|
|
549
|
-
charge(jsonBytes(value));
|
|
550
|
-
return guestJson(value);
|
|
568
|
+
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}`);
|
|
551
569
|
},
|
|
552
570
|
volume: (text) => {
|
|
553
571
|
readAllowed();
|
|
554
572
|
e.usage.readCalls++;
|
|
555
573
|
const params = JSON.parse(String(text));
|
|
556
|
-
|
|
557
|
-
if (page === undefined)
|
|
558
|
-
throw sdkError(program("local_data_missing", `No exported volume page for these parameters: zyrk export --volume-search '${JSON.stringify(Object.fromEntries(params.filter(([k]) => k !== "cursor")))}'`));
|
|
559
|
-
charge(jsonBytes(page));
|
|
560
|
-
return guestJson(page);
|
|
574
|
+
return saved(() => data().volumePage(params), () => data().volumeFailure(params), `No exported volume page for these parameters: zyrk export --volume-search '${JSON.stringify(Object.fromEntries(params.filter(([k]) => k !== "cursor")))}'`);
|
|
561
575
|
},
|
|
562
576
|
encode: (text) => {
|
|
563
577
|
const bytes = Buffer.from(String(text), "utf8");
|