@johannc-dev/zyrk 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +79 -2
- package/dist/build.d.ts +48 -0
- package/dist/build.js +338 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +295 -0
- package/dist/client.d.ts +208 -0
- package/dist/client.js +416 -0
- package/dist/contracts.d.ts +312 -0
- package/dist/contracts.js +154 -0
- package/dist/errors.d.ts +91 -0
- package/dist/errors.js +58 -0
- package/dist/export.d.ts +87 -0
- package/dist/export.js +338 -0
- package/dist/globals.d.ts +59 -0
- package/dist/hourfile.d.ts +57 -0
- package/dist/hourfile.js +338 -0
- package/dist/limits.d.ts +91 -0
- package/dist/limits.js +54 -0
- package/dist/local-data.d.ts +106 -0
- package/dist/local-data.js +345 -0
- package/dist/local-replay.d.ts +40 -0
- package/dist/local-replay.js +342 -0
- package/dist/local-storage.d.ts +76 -0
- package/dist/local-storage.js +499 -0
- package/dist/local.d.ts +235 -0
- package/dist/local.js +1133 -0
- package/dist/sandbox-bootstrap.js +519 -0
- package/dist/sdk.d.ts +1129 -0
- package/dist/sdk.js +208 -0
- package/dist/stream.d.ts +36 -0
- package/dist/stream.js +69 -0
- package/dist/webhook.d.ts +43 -0
- package/dist/webhook.js +54 -0
- package/package.json +56 -4
package/dist/export.d.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
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 or 5xx failures tolerated in a row before giving up (default 8). */
|
|
11
|
+
readonly maxRetries?: number;
|
|
12
|
+
/** Longest wait accepted on a 429 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
|
+
/** Hours that may still change (open, not sealed), files downloaded again. */
|
|
18
|
+
readonly onWarning?: (message: string) => void;
|
|
19
|
+
}
|
|
20
|
+
export interface ExportProgress {
|
|
21
|
+
readonly read: string;
|
|
22
|
+
readonly hour: number | null;
|
|
23
|
+
readonly hours: number;
|
|
24
|
+
readonly done: boolean;
|
|
25
|
+
readonly bytes: number;
|
|
26
|
+
}
|
|
27
|
+
export interface ExportRead {
|
|
28
|
+
readonly scope: "token" | "wallet";
|
|
29
|
+
readonly key: string;
|
|
30
|
+
readonly sinceMs: number;
|
|
31
|
+
readonly untilMs: number;
|
|
32
|
+
readonly side?: "any" | "buy" | "sell";
|
|
33
|
+
readonly withSignatures?: boolean;
|
|
34
|
+
readonly withMarket?: boolean;
|
|
35
|
+
readonly withEvents?: boolean;
|
|
36
|
+
readonly withOrder?: boolean;
|
|
37
|
+
readonly withTx?: boolean;
|
|
38
|
+
/** Download every hour again, even those of a finished export. */
|
|
39
|
+
readonly refresh?: boolean;
|
|
40
|
+
}
|
|
41
|
+
export declare class ExportError extends Error {
|
|
42
|
+
readonly code: string;
|
|
43
|
+
readonly status?: number | undefined;
|
|
44
|
+
readonly name = "ExportError";
|
|
45
|
+
constructor(message: string, code: string, status?: number | undefined);
|
|
46
|
+
}
|
|
47
|
+
export interface ExportSummary {
|
|
48
|
+
readonly reads: readonly ReadMeta[];
|
|
49
|
+
readonly requests: number;
|
|
50
|
+
/** Compressed bytes written. */
|
|
51
|
+
readonly bytes: number;
|
|
52
|
+
}
|
|
53
|
+
/** `[since, until)` in parts of at most 30 days, ending on UTC day boundaries. */
|
|
54
|
+
export declare function parts(sinceMs: number, untilMs: number): [number, number][];
|
|
55
|
+
/** Meta of an export answer (`history_columns::export::hour_response`). */
|
|
56
|
+
interface HourMeta {
|
|
57
|
+
readonly format?: string;
|
|
58
|
+
readonly coverage?: ReadMeta["coverage"];
|
|
59
|
+
readonly batch?: {
|
|
60
|
+
hour: number;
|
|
61
|
+
zstdBytes: number;
|
|
62
|
+
} | null;
|
|
63
|
+
readonly resumeAfterHour?: number | null;
|
|
64
|
+
readonly done?: boolean;
|
|
65
|
+
}
|
|
66
|
+
/** `"ZXH1" | u32 LE meta length | meta JSON | exported hour`. */
|
|
67
|
+
export declare function parseFrame(bytes: Uint8Array): {
|
|
68
|
+
meta: HourMeta;
|
|
69
|
+
file: Uint8Array;
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* The first hour to download again in a finished export: an hour that was
|
|
73
|
+
* open (`live`) or not sealed yet when it was exported may have changed.
|
|
74
|
+
* `null`: nothing to refresh.
|
|
75
|
+
*/
|
|
76
|
+
export declare function staleFrom(meta: ReadMeta, dir: string): number | null;
|
|
77
|
+
/** Exports one read (cut into parts of at most 30 days). Resumes an interrupted export. */
|
|
78
|
+
export declare function exportHistory(connection: ExportConnection, out: string, read: ExportRead): Promise<ExportSummary>;
|
|
79
|
+
/** Saves `/v1/tokens/{mint}/totals` for `ctx.data.tokenTotals`. */
|
|
80
|
+
export declare function exportTotals(connection: ExportConnection, out: string, mint: string, quote?: string | null): Promise<Json>;
|
|
81
|
+
/**
|
|
82
|
+
* Saves every page of `/v1/volume/search` for `params`, following the
|
|
83
|
+
* cursor exactly as `ctx.data.volumeSearch` does, so that the same call in
|
|
84
|
+
* a local run finds each page.
|
|
85
|
+
*/
|
|
86
|
+
export declare function exportVolumeSearch(connection: ExportConnection, out: string, params: VolumeSearchParams): Promise<number>;
|
|
87
|
+
export {};
|
package/dist/export.js
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { decompressHour } from "./hourfile.js";
|
|
4
|
+
import { HOUR_MS, READ_FORMAT, 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
|
+
class Http {
|
|
44
|
+
c;
|
|
45
|
+
requests = 0;
|
|
46
|
+
#last = 0;
|
|
47
|
+
constructor(c) {
|
|
48
|
+
this.c = c;
|
|
49
|
+
}
|
|
50
|
+
async raw(path, query) {
|
|
51
|
+
const f = this.c.fetch ?? fetch;
|
|
52
|
+
const url = new URL(this.c.url.replace(/\/+$/, "") + path);
|
|
53
|
+
for (const [k, v] of Object.entries(query))
|
|
54
|
+
url.searchParams.set(k, v);
|
|
55
|
+
let failures = 0;
|
|
56
|
+
for (;;) {
|
|
57
|
+
if (this.c.rpm) {
|
|
58
|
+
const gap = 60_000 / this.c.rpm - (Date.now() - this.#last);
|
|
59
|
+
if (gap > 0)
|
|
60
|
+
await sleep(gap, this.c.signal);
|
|
61
|
+
}
|
|
62
|
+
this.#last = Date.now();
|
|
63
|
+
this.requests++;
|
|
64
|
+
let response;
|
|
65
|
+
let bytes;
|
|
66
|
+
try {
|
|
67
|
+
response = await f(url, {
|
|
68
|
+
headers: { authorization: `Bearer ${this.c.apiKey}` },
|
|
69
|
+
...(this.c.signal ? { signal: this.c.signal } : {}),
|
|
70
|
+
});
|
|
71
|
+
bytes = new Uint8Array(await response.arrayBuffer());
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
if (this.c.signal?.aborted)
|
|
75
|
+
throw error;
|
|
76
|
+
if (++failures > (this.c.maxRetries ?? 8))
|
|
77
|
+
throw new ExportError(`Network error on ${url.pathname}: ${String(error)}`, "network");
|
|
78
|
+
await sleep(Math.min(30_000, 500 * 2 ** failures), this.c.signal);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
82
|
+
if (response.ok)
|
|
83
|
+
return { status: response.status, contentType, bytes };
|
|
84
|
+
let body = null;
|
|
85
|
+
try {
|
|
86
|
+
body = JSON.parse(new TextDecoder().decode(bytes));
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
body = null;
|
|
90
|
+
}
|
|
91
|
+
const err = body?.error;
|
|
92
|
+
const code = typeof err === "string"
|
|
93
|
+
? err
|
|
94
|
+
: typeof err?.code === "string"
|
|
95
|
+
? err.code
|
|
96
|
+
: `http_${response.status}`;
|
|
97
|
+
const message = err?.message ?? code;
|
|
98
|
+
const after = response.headers.get("retry-after");
|
|
99
|
+
const header = after === null ? Number.NaN : Number(after);
|
|
100
|
+
const bodyRetry = body
|
|
101
|
+
?.retryAfterMs;
|
|
102
|
+
const retryMs = typeof bodyRetry === "number"
|
|
103
|
+
? bodyRetry
|
|
104
|
+
: Number.isFinite(header) && header >= 0
|
|
105
|
+
? header * 1000
|
|
106
|
+
: undefined;
|
|
107
|
+
if (response.status === 429) {
|
|
108
|
+
const wait = retryMs ?? 60_000;
|
|
109
|
+
if (wait > (this.c.maxWaitMs ?? 300_000))
|
|
110
|
+
throw new ExportError(`${code}: ${message} Retry after ${Math.round(wait / 1000)} s: run the same command again then, the export resumes where it stopped.`, code, 429);
|
|
111
|
+
await sleep(wait, this.c.signal);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (response.status >= 500 && code !== "history_timeout") {
|
|
115
|
+
if (++failures > (this.c.maxRetries ?? 8))
|
|
116
|
+
throw new ExportError(`${response.status} ${code}: ${message}`, code, response.status);
|
|
117
|
+
await sleep(retryMs ?? Math.min(30_000, 500 * 2 ** failures), this.c.signal);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
throw new ExportError(`${response.status} ${code}: ${message}`, code, response.status);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
async get(path, query) {
|
|
124
|
+
const answer = await this.raw(path, query);
|
|
125
|
+
const text = new TextDecoder().decode(answer.bytes);
|
|
126
|
+
try {
|
|
127
|
+
return text ? JSON.parse(text) : null;
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
throw new ExportError(`${path}: the answer is not JSON`, "decode");
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
export function parseFrame(bytes) {
|
|
135
|
+
const magic = String.fromCharCode(...bytes.subarray(0, 4));
|
|
136
|
+
if (bytes.length < 8 || magic !== "ZXH1")
|
|
137
|
+
throw new ExportError("Unexpected answer from the history route (not an export frame)", "decode");
|
|
138
|
+
const n = new DataView(bytes.buffer, bytes.byteOffset).getUint32(4, true);
|
|
139
|
+
if (8 + n > bytes.length)
|
|
140
|
+
throw new ExportError("Truncated answer of the history route", "decode");
|
|
141
|
+
const meta = JSON.parse(new TextDecoder().decode(bytes.subarray(8, 8 + n)));
|
|
142
|
+
if (meta.format !== "zyrk-hour/2")
|
|
143
|
+
throw new ExportError(`Unexpected answer from the history route (format ${String(meta.format)}): update the SDK`, "decode");
|
|
144
|
+
const file = bytes.subarray(8 + n);
|
|
145
|
+
if (meta.batch && file.length !== meta.batch.zstdBytes)
|
|
146
|
+
throw new ExportError("Truncated answer of the history route", "decode");
|
|
147
|
+
return { meta, file };
|
|
148
|
+
}
|
|
149
|
+
function loadMeta(file) {
|
|
150
|
+
if (!existsSync(file))
|
|
151
|
+
return undefined;
|
|
152
|
+
try {
|
|
153
|
+
return JSON.parse(readFileSync(file, "utf8"));
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function cleanTemporaries(dir) {
|
|
160
|
+
for (const name of readdirSync(dir))
|
|
161
|
+
if (/\.tmp-\d+$/.test(name))
|
|
162
|
+
rmSync(join(dir, name), { force: true });
|
|
163
|
+
}
|
|
164
|
+
export function staleFrom(meta, dir) {
|
|
165
|
+
const sealed = meta.coverage?.sealedUntilMs ?? 0;
|
|
166
|
+
let from = sealed < meta.untilMs || meta.coverage?.preparing
|
|
167
|
+
? Math.floor(Math.max(sealed, meta.sinceMs) / HOUR_MS) - 1
|
|
168
|
+
: null;
|
|
169
|
+
for (const hour of meta.hours) {
|
|
170
|
+
if (from !== null && hour >= from)
|
|
171
|
+
break;
|
|
172
|
+
let live = false;
|
|
173
|
+
try {
|
|
174
|
+
live =
|
|
175
|
+
decompressHour(readFileSync(join(dir, `${hour}.zhb`))).header.source ===
|
|
176
|
+
"live";
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
live = true;
|
|
180
|
+
}
|
|
181
|
+
if (live) {
|
|
182
|
+
from = hour;
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return from;
|
|
187
|
+
}
|
|
188
|
+
async function exportPart(http, out, s, sinceMs, untilMs, progress, refresh, onProgress, onWarning) {
|
|
189
|
+
const id = readId(s, sinceMs, untilMs);
|
|
190
|
+
const dir = join(out, "reads", id);
|
|
191
|
+
mkdirSync(dir, { recursive: true });
|
|
192
|
+
cleanTemporaries(dir);
|
|
193
|
+
const file = join(dir, "read.json");
|
|
194
|
+
const fresh = {
|
|
195
|
+
format: READ_FORMAT,
|
|
196
|
+
...s,
|
|
197
|
+
sinceMs,
|
|
198
|
+
untilMs,
|
|
199
|
+
coverage: null,
|
|
200
|
+
afterHour: null,
|
|
201
|
+
done: false,
|
|
202
|
+
hours: [],
|
|
203
|
+
hashes: {},
|
|
204
|
+
updatedAt: new Date().toISOString(),
|
|
205
|
+
};
|
|
206
|
+
let meta = refresh ? fresh : (loadMeta(file) ?? fresh);
|
|
207
|
+
if (meta.done) {
|
|
208
|
+
const from = staleFrom(meta, dir);
|
|
209
|
+
if (from !== null) {
|
|
210
|
+
onWarning?.(`${id}: hours from ${new Date(from * HOUR_MS).toISOString()} were open or not sealed when exported; downloading them again`);
|
|
211
|
+
const hours = meta.hours.filter((h) => h < from);
|
|
212
|
+
meta = {
|
|
213
|
+
...meta,
|
|
214
|
+
done: false,
|
|
215
|
+
afterHour: hours.length ? from - 1 : null,
|
|
216
|
+
hours,
|
|
217
|
+
hashes: Object.fromEntries(Object.entries(meta.hashes ?? {}).filter(([h]) => Number(h) < from)),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const save = () => writeAtomic(file, JSON.stringify(meta, null, 1));
|
|
222
|
+
save();
|
|
223
|
+
while (!meta.done) {
|
|
224
|
+
const query = {
|
|
225
|
+
since_ms: String(sinceMs),
|
|
226
|
+
until_ms: String(untilMs),
|
|
227
|
+
side: s.side,
|
|
228
|
+
allow_partial: "true",
|
|
229
|
+
with_signatures: String(s.withSignatures),
|
|
230
|
+
with_market: String(s.withMarket),
|
|
231
|
+
with_events: String(s.withEvents),
|
|
232
|
+
with_order: String(s.withOrder),
|
|
233
|
+
with_tx: String(!!s.withTx),
|
|
234
|
+
};
|
|
235
|
+
if (meta.afterHour !== null)
|
|
236
|
+
query.after_hour = String(meta.afterHour);
|
|
237
|
+
const answer = await http.raw(`/v1/functions/history/${s.scope}/${encodeURIComponent(s.key)}`, query);
|
|
238
|
+
const { meta: m, file: bytes } = parseFrame(answer.bytes);
|
|
239
|
+
const coverage = m.coverage ?? meta.coverage;
|
|
240
|
+
if (!m.batch) {
|
|
241
|
+
if (typeof m.resumeAfterHour === "number") {
|
|
242
|
+
meta = { ...meta, coverage, afterHour: m.resumeAfterHour };
|
|
243
|
+
save();
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
const sealed = coverage?.sealedUntilMs ?? 0;
|
|
247
|
+
meta = {
|
|
248
|
+
...meta,
|
|
249
|
+
coverage,
|
|
250
|
+
done: true,
|
|
251
|
+
updatedAt: new Date().toISOString(),
|
|
252
|
+
};
|
|
253
|
+
save();
|
|
254
|
+
if (sealed < untilMs)
|
|
255
|
+
onWarning?.(`${id}: history after ${new Date(sealed).toISOString()} was not sealed yet; run zyrk export again later to get its final hours`);
|
|
256
|
+
onProgress?.({
|
|
257
|
+
read: id,
|
|
258
|
+
hour: null,
|
|
259
|
+
hours: meta.hours.length,
|
|
260
|
+
done: true,
|
|
261
|
+
bytes: progress.bytes,
|
|
262
|
+
});
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
const { hour } = m.batch;
|
|
266
|
+
let header;
|
|
267
|
+
try {
|
|
268
|
+
header = decompressHour(bytes).header;
|
|
269
|
+
}
|
|
270
|
+
catch (e) {
|
|
271
|
+
throw new ExportError(`Hour ${hour}: ${e.message}`, "decode");
|
|
272
|
+
}
|
|
273
|
+
if (header.hour !== hour)
|
|
274
|
+
throw new ExportError(`Hour ${hour}: inconsistent file`, "decode");
|
|
275
|
+
if (meta.afterHour !== null && hour <= meta.afterHour)
|
|
276
|
+
throw new ExportError(`Hour ${hour} came twice`, "decode");
|
|
277
|
+
writeAtomic(join(dir, `${hour}.zhb`), bytes);
|
|
278
|
+
progress.bytes += bytes.length;
|
|
279
|
+
meta = {
|
|
280
|
+
...meta,
|
|
281
|
+
coverage,
|
|
282
|
+
afterHour: hour,
|
|
283
|
+
hours: [...meta.hours, hour],
|
|
284
|
+
hashes: { ...meta.hashes, [String(hour)]: sha256(bytes) },
|
|
285
|
+
updatedAt: new Date().toISOString(),
|
|
286
|
+
};
|
|
287
|
+
save();
|
|
288
|
+
onProgress?.({
|
|
289
|
+
read: id,
|
|
290
|
+
hour,
|
|
291
|
+
hours: meta.hours.length,
|
|
292
|
+
done: false,
|
|
293
|
+
bytes: progress.bytes,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
return meta;
|
|
297
|
+
}
|
|
298
|
+
export async function exportHistory(connection, out, read) {
|
|
299
|
+
const http = new Http(connection);
|
|
300
|
+
const s = readSpec(read);
|
|
301
|
+
const progress = { bytes: 0 };
|
|
302
|
+
const reads = [];
|
|
303
|
+
for (const [a, b] of parts(read.sinceMs, read.untilMs)) {
|
|
304
|
+
if (b - a > MAX_WINDOW_MS)
|
|
305
|
+
throw new ExportError("part too long", "bug");
|
|
306
|
+
reads.push(await exportPart(http, out, s, a, b, progress, !!read.refresh, connection.onProgress, connection.onWarning));
|
|
307
|
+
}
|
|
308
|
+
return { reads, requests: http.requests, bytes: progress.bytes };
|
|
309
|
+
}
|
|
310
|
+
export async function exportTotals(connection, out, mint, quote = null) {
|
|
311
|
+
const http = new Http(connection);
|
|
312
|
+
const body = await http.get(`/v1/tokens/${encodeURIComponent(mint)}/totals`, quote ? { quote } : {});
|
|
313
|
+
mkdirSync(join(out, "totals"), { recursive: true });
|
|
314
|
+
writeAtomic(join(out, "totals", totalsName(mint, quote)), JSON.stringify(body));
|
|
315
|
+
return body;
|
|
316
|
+
}
|
|
317
|
+
export async function exportVolumeSearch(connection, out, params) {
|
|
318
|
+
const http = new Http(connection);
|
|
319
|
+
mkdirSync(join(out, "volume"), { recursive: true });
|
|
320
|
+
let p = { ...params };
|
|
321
|
+
for (let pages = 1;; pages++) {
|
|
322
|
+
const flat = Object.entries(p)
|
|
323
|
+
.filter(([, v]) => v !== undefined && v !== null)
|
|
324
|
+
.map(([k, v]) => [k, String(v)]);
|
|
325
|
+
const page = await http.get("/v1/volume/search", Object.fromEntries(flat));
|
|
326
|
+
writeAtomic(join(out, "volume", `${volumeName(flat)}.json`), JSON.stringify({ params: flat, page }));
|
|
327
|
+
const next = page?.nextCursor;
|
|
328
|
+
if (next === null || next === undefined)
|
|
329
|
+
return pages;
|
|
330
|
+
if (typeof next !== "string" || next === p.cursor)
|
|
331
|
+
throw new ExportError("Invalid next cursor", "decode");
|
|
332
|
+
const w = page
|
|
333
|
+
.window;
|
|
334
|
+
if (w && typeof w === "object" && "since" in w)
|
|
335
|
+
p = { ...p, since: w.since, until: w.until };
|
|
336
|
+
p = { ...p, cursor: next };
|
|
337
|
+
}
|
|
338
|
+
}
|
|
@@ -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,57 @@
|
|
|
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
|
+
readonly order: boolean;
|
|
16
|
+
/** Absent from files written before transaction ids. */
|
|
17
|
+
readonly tx?: boolean;
|
|
18
|
+
readonly columns: readonly (readonly [string, ColumnType, number])[];
|
|
19
|
+
}
|
|
20
|
+
/** Typed array constructors of the realm the batch is built for (a `vm` context, or Node's own). */
|
|
21
|
+
export interface Realm {
|
|
22
|
+
readonly Uint8Array: Uint8ArrayConstructor;
|
|
23
|
+
readonly Uint16Array: Uint16ArrayConstructor;
|
|
24
|
+
readonly Uint32Array: Uint32ArrayConstructor;
|
|
25
|
+
readonly Int32Array: Int32ArrayConstructor;
|
|
26
|
+
readonly BigUint64Array: BigUint64ArrayConstructor;
|
|
27
|
+
readonly Float64Array: Float64ArrayConstructor;
|
|
28
|
+
readonly Array: ArrayConstructor;
|
|
29
|
+
readonly Object: ObjectConstructor;
|
|
30
|
+
}
|
|
31
|
+
export declare const nodeRealm: Realm;
|
|
32
|
+
/** Column names and types, in the order of the sandbox's batch object. */
|
|
33
|
+
export declare const COLUMNS: readonly (readonly [string, ColumnType])[];
|
|
34
|
+
export declare const MARKET_COLUMNS: readonly (readonly [string, ColumnType])[];
|
|
35
|
+
export declare const ORDER_COLUMNS: readonly (readonly [string, ColumnType])[];
|
|
36
|
+
export declare const TX_COLUMNS: readonly (readonly [string, ColumnType])[];
|
|
37
|
+
export declare class HourFileError extends Error {
|
|
38
|
+
readonly name = "HourFileError";
|
|
39
|
+
}
|
|
40
|
+
/** A decoded `ZHB1` buffer: its header and the raw bytes behind its columns. */
|
|
41
|
+
export interface RawHour {
|
|
42
|
+
readonly header: HourHeader;
|
|
43
|
+
readonly bytes: Uint8Array;
|
|
44
|
+
}
|
|
45
|
+
export declare function decompressHour(file: Uint8Array): RawHour;
|
|
46
|
+
/** Checks the header and every column, then the address indexes (see {@link checkIndexes}). */
|
|
47
|
+
export declare function parseHour(bytes: Uint8Array): RawHour;
|
|
48
|
+
/**
|
|
49
|
+
* The batch of a raw hour, every column in its own buffer (as in the
|
|
50
|
+
* sandbox), arrays of `realm`. Property order is the sandbox's.
|
|
51
|
+
*/
|
|
52
|
+
export declare function batchOf(raw: RawHour, realm?: Realm): HourBatch;
|
|
53
|
+
/** The `ZHB1` buffer of a batch (what the server writes; used by tests and local re-cuts). */
|
|
54
|
+
export declare function encodeHour(batch: HourBatch): Uint8Array;
|
|
55
|
+
/** zstd with the frame checksum, as the server writes exported hours. */
|
|
56
|
+
export declare function compress(raw: Uint8Array): Uint8Array;
|
|
57
|
+
export declare function compressHour(batch: HourBatch): Uint8Array;
|