@solard/cli 0.2.17 → 0.2.20

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.
@@ -1,3 +1,4 @@
1
+ import { PublicKey, type ConfirmedSignatureInfo } from "@solana/web3.js";
1
2
  import type { Solard } from "@solard/core";
2
3
  import {
3
4
  analyzeTokenHistory,
@@ -9,6 +10,12 @@ import {
9
10
  type TokenHistoryTrade,
10
11
  } from "@solard/core";
11
12
 
13
+ import {
14
+ replaceResearchHistoryIndex,
15
+ researchHistoryCachePath,
16
+ type ResearchHistorySignature,
17
+ } from "./research-history-cache.ts";
18
+
12
19
  export type TokenHistoryCliFlags = Map<string, string>;
13
20
 
14
21
  type Emit = (value: string) => void;
@@ -135,12 +142,170 @@ function tradeLine(
135
142
  );
136
143
  }
137
144
 
145
+ async function scanHistoryAddress(args: {
146
+ slrd: Solard;
147
+ kind: "curve" | "pool";
148
+ address: string;
149
+ maxSignatures: number;
150
+ progress: (line: string) => void;
151
+ }): Promise<{
152
+ rows: ResearchHistorySignature[];
153
+ reachedStart: boolean;
154
+ truncated: boolean;
155
+ }> {
156
+ const rows: ConfirmedSignatureInfo[] = [];
157
+ let before: string | undefined;
158
+ let pages = 0;
159
+ let reachedStart = false;
160
+ let truncated = false;
161
+ while (true) {
162
+ const remaining =
163
+ args.maxSignatures > 0 ? args.maxSignatures - rows.length : 1_000;
164
+ if (args.maxSignatures > 0 && remaining <= 0) {
165
+ truncated = true;
166
+ break;
167
+ }
168
+ const limit = Math.min(1_000, args.maxSignatures > 0 ? remaining : 1_000);
169
+ const page = await args.slrd
170
+ .connection()
171
+ .getSignaturesForAddress(
172
+ new PublicKey(args.address),
173
+ { limit, ...(before ? { before } : {}) },
174
+ "finalized",
175
+ );
176
+ pages += 1;
177
+ rows.push(...page.filter((row) => !row.err));
178
+ if (pages === 1 || pages % 5 === 0)
179
+ args.progress(
180
+ `SIGS ${args.kind.padEnd(5)} pages=${pages} signatures=${rows.length}`,
181
+ );
182
+ if (page.length < limit) {
183
+ reachedStart = true;
184
+ break;
185
+ }
186
+ const next = page.at(-1)?.signature;
187
+ if (!next || next === before) {
188
+ reachedStart = true;
189
+ break;
190
+ }
191
+ before = next;
192
+ }
193
+ return {
194
+ rows: [...rows].reverse().map((row, index) => ({
195
+ mint: "",
196
+ signature: row.signature,
197
+ slot: row.slot,
198
+ blockTime: row.blockTime ?? null,
199
+ scanKind: args.kind,
200
+ scanAddress: args.address,
201
+ localChronologicalOrder: index,
202
+ })),
203
+ reachedStart,
204
+ truncated,
205
+ };
206
+ }
207
+
208
+ async function runIndexBackfill(args: {
209
+ slrd: Solard;
210
+ mint: string;
211
+ flags: TokenHistoryCliFlags;
212
+ emit: Emit;
213
+ }): Promise<void> {
214
+ const jsonMode = args.flags.has("json");
215
+ const progress = (line: string) => {
216
+ if (!jsonMode) process.stderr.write(`${line}\n`);
217
+ };
218
+ progress(`TOKEN INDEX ${args.mint}`);
219
+ const token = await args.slrd.addToken(args.mint);
220
+ if (!token.bondingCurve)
221
+ throw new Error(`Token ${args.mint} has no Pump bonding curve to index`);
222
+ const supply = await args.slrd
223
+ .connection()
224
+ .getTokenSupply(new PublicKey(args.mint), "confirmed");
225
+ const maxSignatures = Math.max(
226
+ 0,
227
+ Math.trunc(numberFlag(args.flags, "max-signatures", 0) ?? 0),
228
+ );
229
+ const curve = await scanHistoryAddress({
230
+ slrd: args.slrd,
231
+ kind: "curve",
232
+ address: token.bondingCurve,
233
+ maxSignatures,
234
+ progress,
235
+ });
236
+ const pool = token.pool
237
+ ? await scanHistoryAddress({
238
+ slrd: args.slrd,
239
+ kind: "pool",
240
+ address: token.pool,
241
+ maxSignatures,
242
+ progress,
243
+ })
244
+ : null;
245
+ const unique = new Map<string, ResearchHistorySignature>();
246
+ for (const row of [...curve.rows, ...(pool?.rows ?? [])]) {
247
+ const value = { ...row, mint: args.mint };
248
+ const previous = unique.get(value.signature);
249
+ if (!previous || value.scanKind === "curve")
250
+ unique.set(value.signature, value);
251
+ }
252
+ const rows = [...unique.values()].sort(
253
+ (a, b) =>
254
+ a.slot - b.slot ||
255
+ (a.blockTime ?? 0) - (b.blockTime ?? 0) ||
256
+ a.localChronologicalOrder - b.localChronologicalOrder ||
257
+ a.signature.localeCompare(b.signature),
258
+ );
259
+ rows.forEach((row, index) => {
260
+ row.localChronologicalOrder = index;
261
+ });
262
+ const fromCreation = curve.reachedStart && !curve.truncated;
263
+ const meta = {
264
+ version: 1 as const,
265
+ mint: args.mint,
266
+ decimals: supply.value.decimals,
267
+ supplyUi: Number(supply.value.uiAmountString ?? "0"),
268
+ quoteMint: token.quoteMint ?? null,
269
+ bondingCurve: token.bondingCurve,
270
+ pool: token.pool ?? null,
271
+ fromCreation,
272
+ indexedSignatures: rows.length,
273
+ indexedAtMs: Date.now(),
274
+ };
275
+ replaceResearchHistoryIndex({ mint: args.mint, rows, meta });
276
+ const result = {
277
+ ...meta,
278
+ curveSignatures: curve.rows.length,
279
+ poolSignatures: pool?.rows.length ?? 0,
280
+ cache: researchHistoryCachePath(),
281
+ };
282
+ if (jsonMode) args.emit(`${JSON.stringify(result)}\n`);
283
+ else
284
+ args.emit(
285
+ [
286
+ "",
287
+ "TOKEN HISTORY INDEX",
288
+ `Mint: ${args.mint}`,
289
+ `Signatures: ${rows.length}`,
290
+ `From creation: ${fromCreation ? "yes" : "no"}`,
291
+ `Transactions read: 0`,
292
+ `Research cache: ${researchHistoryCachePath()}`,
293
+ "",
294
+ "READY: scripts hydrate only the historical intervals they request.",
295
+ ].join("\n") + "\n",
296
+ );
297
+ }
298
+
138
299
  async function runBackfill(args: {
139
300
  slrd: Solard;
140
301
  mint: string;
141
302
  flags: TokenHistoryCliFlags;
142
303
  emit: Emit;
143
304
  }): Promise<void> {
305
+ if (!args.flags.has("exact") && !args.flags.has("materialize")) {
306
+ await runIndexBackfill(args);
307
+ return;
308
+ }
144
309
  const jsonMode = args.flags.has("json");
145
310
  const progress = (line: string) => {
146
311
  if (!jsonMode) process.stderr.write(`${line}\n`);
@@ -157,53 +322,53 @@ async function runBackfill(args: {
157
322
  rpcRetries: numberFlag(args.flags, "rpc-retries", 2),
158
323
  retryDelayMs: numberFlag(args.flags, "retry-delay-ms", 750),
159
324
  maxSignaturesPerAddress: numberFlag(args.flags, "max-signatures", 0),
325
+ priceSampleMs: args.flags.has("exact")
326
+ ? 0
327
+ : numberFlag(args.flags, "price-sample-ms", 1_000),
160
328
  replace: args.flags.has("replace"),
161
329
  onProgress: (row) => {
162
330
  if (row.phase === "signatures") {
163
- if (row.pages === 1 || row.pages % 5 === 0) {
331
+ if (row.pages === 1 || row.pages % 5 === 0)
164
332
  progress(
165
333
  `SIGS ${row.kind.padEnd(5)} pages=${row.pages} signatures=${row.signatures}`,
166
334
  );
167
- }
335
+ } else if (row.phase === "sample") {
336
+ progress(
337
+ `SAMPLE ${row.selected}/${row.total} signatures @ ${row.sampleMs}ms`,
338
+ );
168
339
  } else if (row.phase === "transactions") {
169
340
  if (
170
341
  row.completed === row.total ||
171
342
  row.completed <= row.batchSize ||
172
343
  row.completed % 500 === 0
173
- ) {
344
+ )
174
345
  progress(`TX ${row.completed}/${row.total}`);
175
- }
176
346
  } else if (row.phase === "retry") {
177
347
  progress(
178
- `RETRY ${row.operation}${row.kind ? `:${row.kind}` : ""} ` +
179
- `${row.attempt}/${row.maxAttempts} ${row.error}`,
348
+ `RETRY ${row.operation}${row.kind ? `:${row.kind}` : ""} ${row.attempt}/${row.maxAttempts} ${row.error}`,
180
349
  );
181
350
  } else if (row.phase === "rpc-error") {
182
351
  progress(
183
- `RPCERR ${row.operation}${row.kind ? `:${row.kind}` : ""} ` +
184
- `failed=${row.failedItems} ${row.error}`,
352
+ `RPCERR ${row.operation}${row.kind ? `:${row.kind}` : ""} failed=${row.failedItems} ${row.error}`,
185
353
  );
186
354
  } else if (row.phase === "throttle") {
187
355
  progress(
188
- `THROT ${row.reason} wait=${row.waitMs}ms ` +
189
- `batch=${row.batchSize}->${row.nextBatchSize}`,
356
+ `THROT ${row.reason} wait=${row.waitMs}ms batch=${row.batchSize}->${row.nextBatchSize}`,
190
357
  );
191
358
  } else if (row.phase === "parse") {
192
359
  if (
193
360
  row.completed === row.total ||
194
361
  row.completed === 1 ||
195
362
  row.completed % 1_000 === 0
196
- ) {
363
+ )
197
364
  progress(
198
365
  `PARSE ${row.completed}/${row.total} trades=${row.trades} ambiguous=${row.ambiguous}`,
199
366
  );
200
- }
201
367
  } else if (row.phase === "store") {
202
- if (row.completed === row.total || row.completed % 1_000 === 0) {
368
+ if (row.completed === row.total || row.completed % 1_000 === 0)
203
369
  progress(
204
370
  `STORE ${row.completed}/${row.total} inserted=${row.inserted} updated=${row.updated}`,
205
371
  );
206
- }
207
372
  } else if (row.phase === "candles") {
208
373
  progress(`1S trades=${row.trades} sparse-candles=${row.candles}`);
209
374
  }
@@ -225,28 +390,16 @@ async function runBackfill(args: {
225
390
  args.emit(
226
391
  [
227
392
  "",
228
- "TOKEN HISTORY BACKFILL",
393
+ "TOKEN HISTORY MATERIALIZATION",
229
394
  `Mint: ${result.mint}`,
230
395
  `Coverage: ${coverageText(result)}`,
231
396
  `Creation: ${fmtDate(result.creationAtMs)} ${result.creationSignature ? compact(result.creationSignature, 10, 10) : "-"}`,
232
- result.venueFamily === "raydium"
233
- ? `LaunchLab pool: ${result.launchLabPool ?? "not found / history begins after launch"}`
234
- : `Bonding curve: ${result.bondingCurve}`,
235
- result.venueFamily === "raydium"
236
- ? `Raydium pools: ${(result.raydiumPools ?? []).join(", ") || "none discovered"}`
237
- : `PumpSwap pool: ${result.pool ?? "not observed / not graduated"}`,
238
397
  `Signatures: ${result.uniqueSignatures}`,
239
- `Parsed tx: ${result.parsedTransactions}`,
240
- `Missing tx: ${result.missingTransactions}`,
241
- `Failed tx lookups: ${result.failedTransactions}`,
398
+ `History mode: ${result.historyMode ?? "exact"}${result.priceSampleMs ? ` @ ${result.priceSampleMs}ms` : ""}`,
399
+ `Hydrated tx: ${result.parsedTransactions}/${result.uniqueSignatures}`,
242
400
  `Stored trades: ${result.storedTrades}`,
243
401
  `Sparse 1s candles: ${result.storedCandles1s}`,
244
- `Inserted/updated: ${result.insertedTrades}/${result.updatedTrades}`,
245
- `Ambiguous skipped: ${result.skippedAmbiguous}`,
246
402
  "",
247
- result.complete
248
- ? "READY: durable tape is proven complete from creation."
249
- : "WARNING: tape is not proven complete; inspect the gaps above before strict backtesting.",
250
403
  ].join("\n") + "\n",
251
404
  );
252
405
  }
@@ -514,7 +667,7 @@ export async function runTokenHistoryCommand(args: {
514
667
  throw new Error(
515
668
  `Usage: slrd token ${args.action} <CA> ${
516
669
  args.action === "backfill"
517
- ? "[--replace] [--confirmed] [--json]"
670
+ ? "[--exact|--materialize --price-sample-ms 1000] [--max-signatures N] [--json]"
518
671
  : args.action === "trades"
519
672
  ? "[--from-start] [--min-sol N] [--owner wallet] [--json]"
520
673
  : "[--top N] [--first N] [--json]"