@secondlayer/cli 12.0.0 → 12.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 +40 -3
- package/dist/cli.js +292 -48
- package/dist/cli.js.map +11 -9
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -51,7 +51,44 @@ No account. Writes `.env.local`, restores history, prints the Stacks observer st
|
|
|
51
51
|
| `secondlayer bootstrap --against <manifest> [--to-block <n>] [--public-key <pem>] [-y] [--json]` | Restore chain history from a verified archive into an empty database. Exit `0` restored, `1` diverged, `2` refused |
|
|
52
52
|
| `secondlayer observer [--mode indexer\|signer-shared] [--endpoint host:port] [--recovery journal\|archive] [--network …]` | Print the `[[events_observer]]` stanza. Signer-shared requires `--recovery` |
|
|
53
53
|
| `secondlayer verify [all\|raw\|decode:<name>\|subgraph:<name>] --against <manifest> [--quick\|--deep\|--anchor]` | Compare local data to a signed archive. Default target `raw`. Exit `0` clean, `1` diverged, `2` unanchored |
|
|
54
|
-
| `secondlayer repair --against <archive> [--apply]` | Plan (default) or apply an archive repair |
|
|
54
|
+
| `secondlayer repair --against <archive> [--apply] [-y]` | Plan (default) or apply an archive repair |
|
|
55
|
+
|
|
56
|
+
Bootstrap and repair against the official hosted archive (`archive.secondlayer.tools`)
|
|
57
|
+
are metered per partition; against any other manifest (a mirror, a teammate's
|
|
58
|
+
box, a local file) they are free. See [Metered fetches](#metered-fetches).
|
|
59
|
+
|
|
60
|
+
### Metered fetches
|
|
61
|
+
|
|
62
|
+
`secondlayer bootstrap` and `secondlayer repair` pull partitions from the
|
|
63
|
+
signed archive instead of replaying the chain, and that pull costs money only
|
|
64
|
+
when it targets the official hosted archive. Point `--against` at a mirror or
|
|
65
|
+
a local manifest and nothing is charged, nothing is even contacted beyond
|
|
66
|
+
that manifest, because self-hosting the archive is a supported way to use
|
|
67
|
+
these commands, not a workaround.
|
|
68
|
+
|
|
69
|
+
Against the official host, both commands quote before they charge:
|
|
70
|
+
|
|
71
|
+
1. The manifest's partition list is priced with a free, no-write call
|
|
72
|
+
(`POST /api/archive/quote`).
|
|
73
|
+
2. The quote prints into the existing plan output, for example
|
|
74
|
+
`metered: 528 partitions ≈ $44.00 · balance $50.00`, or for `repair`
|
|
75
|
+
inside its monthly allowance, `metered: free (4 of 6 monthly repair
|
|
76
|
+
fetches remaining)`.
|
|
77
|
+
3. You confirm, or pass `-y` to skip the prompt. `-y` never skips the quote
|
|
78
|
+
or the balance check: if the balance is short, the command exits before
|
|
79
|
+
any partition is fetched and prints the shortfall and
|
|
80
|
+
`secondlayer credits buy`.
|
|
81
|
+
4. Only then does the command fetch, and only the partitions it actually
|
|
82
|
+
reads are charged.
|
|
83
|
+
|
|
84
|
+
A partition already charged in the last 24 hours re-presigns for free, so a
|
|
85
|
+
resumed or retried bootstrap never pays twice for the same bytes. `repair`
|
|
86
|
+
gets 6 free range-bundles a month; `bootstrap` does not, since it is the
|
|
87
|
+
whole-chain operation the free tier exists to not subsidize.
|
|
88
|
+
|
|
89
|
+
`secondlayer verify` is unaffected: it reads manifests and digests, never
|
|
90
|
+
partition bytes, and stays free and anonymous no matter which archive it
|
|
91
|
+
points at.
|
|
55
92
|
|
|
56
93
|
### Subgraphs
|
|
57
94
|
|
|
@@ -181,8 +218,8 @@ const supply = await token.vars.totalSupply.get()
|
|
|
181
218
|
## Frozen commands
|
|
182
219
|
|
|
183
220
|
Local-dev periphery: still functional, hidden from `secondlayer --help`, no further
|
|
184
|
-
investment — may be removed in a future major.
|
|
185
|
-
|
|
221
|
+
investment — may be removed in a future major. `secondlayer subgraphs
|
|
222
|
+
create/deploy` against your own instance is the supported path.
|
|
186
223
|
|
|
187
224
|
- `secondlayer local up/down/start/stop/restart/status/logs` — local stack + dev services
|
|
188
225
|
- `secondlayer local node ...` — local Stacks node management
|
package/dist/cli.js
CHANGED
|
@@ -34320,14 +34320,14 @@ init_esm();
|
|
|
34320
34320
|
// package.json
|
|
34321
34321
|
var package_default = {
|
|
34322
34322
|
name: "@secondlayer/cli",
|
|
34323
|
-
version: "12.
|
|
34323
|
+
version: "12.2.0",
|
|
34324
|
+
description: "Run a Stacks index on your own hardware — set it up, restore verified history, and keep it current.",
|
|
34324
34325
|
repository: {
|
|
34325
34326
|
type: "git",
|
|
34326
34327
|
url: "git+https://github.com/ryanwaits/secondlayer.git",
|
|
34327
34328
|
directory: "packages/cli"
|
|
34328
34329
|
},
|
|
34329
34330
|
homepage: "https://github.com/ryanwaits/secondlayer/tree/main/packages/cli#readme",
|
|
34330
|
-
description: "CLI for subgraphs and blockchain indexing on Stacks",
|
|
34331
34331
|
type: "module",
|
|
34332
34332
|
bin: {
|
|
34333
34333
|
secondlayer: "./dist/cli.js",
|
|
@@ -34363,10 +34363,10 @@ var package_default = {
|
|
|
34363
34363
|
"@inquirer/prompts": "^8.2.0",
|
|
34364
34364
|
"@secondlayer/bundler": "^0.5.1",
|
|
34365
34365
|
"@secondlayer/scaffold": "^1.5.4",
|
|
34366
|
-
"@secondlayer/sdk": "^8.0.
|
|
34367
|
-
"@secondlayer/shared": "^
|
|
34366
|
+
"@secondlayer/sdk": "^8.0.1",
|
|
34367
|
+
"@secondlayer/shared": "^10.0.1",
|
|
34368
34368
|
"@secondlayer/stacks": "^4.0.0",
|
|
34369
|
-
"@secondlayer/subgraphs": "^4.0.
|
|
34369
|
+
"@secondlayer/subgraphs": "^4.0.2",
|
|
34370
34370
|
esbuild: "^0.27.4",
|
|
34371
34371
|
"smol-toml": "^1.6.1",
|
|
34372
34372
|
zod: "^4.3.6"
|
|
@@ -38217,9 +38217,22 @@ async function resolveArchivePublicKey(input4) {
|
|
|
38217
38217
|
}
|
|
38218
38218
|
return;
|
|
38219
38219
|
}
|
|
38220
|
-
async function
|
|
38220
|
+
async function fetchViaGate(gate, partition, retried) {
|
|
38221
|
+
const url = await gate.getUrl(partition.path, retried ? { forceRefresh: true } : undefined);
|
|
38222
|
+
const response2 = await fetch(url, { signal: AbortSignal.timeout(300000) });
|
|
38223
|
+
if (response2.status === 403 && !retried) {
|
|
38224
|
+
return fetchViaGate(gate, partition, true);
|
|
38225
|
+
}
|
|
38226
|
+
if (!response2.ok) {
|
|
38227
|
+
throw new Error(`could not fetch ${partition.path} (${response2.status} ${response2.statusText})`);
|
|
38228
|
+
}
|
|
38229
|
+
return Buffer.from(await response2.arrayBuffer());
|
|
38230
|
+
}
|
|
38231
|
+
async function fetchVerifiedPartition(reference, partition, gate) {
|
|
38221
38232
|
let bytes;
|
|
38222
|
-
if (
|
|
38233
|
+
if (gate) {
|
|
38234
|
+
bytes = await fetchViaGate(gate, partition, false);
|
|
38235
|
+
} else if (reference.isRemote) {
|
|
38223
38236
|
const url = `${reference.root.replace(/\/$/, "")}/${partition.path}`;
|
|
38224
38237
|
const response2 = await fetch(url, { signal: AbortSignal.timeout(300000) });
|
|
38225
38238
|
if (!response2.ok) {
|
|
@@ -38448,11 +38461,123 @@ import { unlink as unlink2, writeFile as writeFile5 } from "node:fs/promises";
|
|
|
38448
38461
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
38449
38462
|
import { join as join15 } from "node:path";
|
|
38450
38463
|
import { ParquetReader } from "@dsnp/parquetjs";
|
|
38464
|
+
import { confirm as confirm7 } from "@inquirer/prompts";
|
|
38451
38465
|
import {
|
|
38452
38466
|
compareRangeDigests as compareRangeDigests2,
|
|
38453
38467
|
computeRangeDigest as computeRangeDigest2
|
|
38454
38468
|
} from "@secondlayer/shared/archive/range-digest";
|
|
38455
38469
|
import { getDb as getDb3, sql as sql2 } from "@secondlayer/shared/db";
|
|
38470
|
+
|
|
38471
|
+
// src/lib/archive-gate.ts
|
|
38472
|
+
init_http();
|
|
38473
|
+
var OFFICIAL_ARCHIVE_HOST = "archive.secondlayer.tools";
|
|
38474
|
+
function isOfficialArchive(reference) {
|
|
38475
|
+
if (!reference.isRemote)
|
|
38476
|
+
return false;
|
|
38477
|
+
try {
|
|
38478
|
+
return new URL(reference.origin).hostname === OFFICIAL_ARCHIVE_HOST;
|
|
38479
|
+
} catch {
|
|
38480
|
+
return false;
|
|
38481
|
+
}
|
|
38482
|
+
}
|
|
38483
|
+
var defaultDeps = { httpArchiveOps };
|
|
38484
|
+
function quoteFromBody(body) {
|
|
38485
|
+
return {
|
|
38486
|
+
partitions: body.partitions,
|
|
38487
|
+
bundles: body.bundles,
|
|
38488
|
+
usdMicros: body.usd_micros,
|
|
38489
|
+
usd: body.usd,
|
|
38490
|
+
freeAllowanceAppliedMicros: body.free_allowance_applied_micros,
|
|
38491
|
+
allowanceRemainingBundles: body.allowance_remaining_bundles,
|
|
38492
|
+
balanceUsdMicros: body.balance_usd_micros,
|
|
38493
|
+
sufficient: body.sufficient
|
|
38494
|
+
};
|
|
38495
|
+
}
|
|
38496
|
+
async function quoteArchiveFetch(paths, flow, deps = defaultDeps) {
|
|
38497
|
+
try {
|
|
38498
|
+
const body = await deps.httpArchiveOps("/api/archive/quote", { method: "POST", body: { paths, flow } });
|
|
38499
|
+
return { ok: true, quote: quoteFromBody(body) };
|
|
38500
|
+
} catch (err) {
|
|
38501
|
+
if (err instanceof CliHttpError) {
|
|
38502
|
+
if (err.status === 503)
|
|
38503
|
+
return { ok: false, kind: "not_configured" };
|
|
38504
|
+
if (err.status === 401) {
|
|
38505
|
+
return { ok: false, kind: "not_authed", message: err.message };
|
|
38506
|
+
}
|
|
38507
|
+
return { ok: false, kind: "error", message: err.message };
|
|
38508
|
+
}
|
|
38509
|
+
throw err;
|
|
38510
|
+
}
|
|
38511
|
+
}
|
|
38512
|
+
var BUY_CREDITS_COMMAND = "secondlayer credits buy";
|
|
38513
|
+
function formatInsufficientMessage(quote) {
|
|
38514
|
+
const shortfallUsd = ((quote.usdMicros - quote.balanceUsdMicros) / 1e6).toFixed(2);
|
|
38515
|
+
const balanceUsd = (quote.balanceUsdMicros / 1e6).toFixed(2);
|
|
38516
|
+
return `Insufficient archive credits: quote $${quote.usd}, balance $${balanceUsd}, short $${shortfallUsd}. Buy more with \`${BUY_CREDITS_COMMAND}\`.`;
|
|
38517
|
+
}
|
|
38518
|
+
var ARCHIVE_GATE_NOT_CONFIGURED_MESSAGE = "the archive gate is not configured on the server; contact the operator";
|
|
38519
|
+
var ALLOWANCE_MONTHLY_BUNDLES = 6;
|
|
38520
|
+
function formatQuoteValue(quote, flow) {
|
|
38521
|
+
if (flow === "repair" && quote.usdMicros === 0 && quote.freeAllowanceAppliedMicros > 0) {
|
|
38522
|
+
return `free (${quote.allowanceRemainingBundles} of ${ALLOWANCE_MONTHLY_BUNDLES} monthly repair fetches remaining)`;
|
|
38523
|
+
}
|
|
38524
|
+
const balance = `$${(quote.balanceUsdMicros / 1e6).toFixed(2)}`;
|
|
38525
|
+
return `${quote.partitions} partitions ≈ $${quote.usd} · balance ${balance}`;
|
|
38526
|
+
}
|
|
38527
|
+
function shouldPromptForGatedFetch(opts) {
|
|
38528
|
+
return !opts.yes && !opts.json;
|
|
38529
|
+
}
|
|
38530
|
+
var FETCH_BATCH_SIZE = 16;
|
|
38531
|
+
function createGatedFetcher(paths, flow, deps = defaultDeps) {
|
|
38532
|
+
const cache = new Map;
|
|
38533
|
+
const batchOf = new Map;
|
|
38534
|
+
for (const [index, path11] of paths.entries()) {
|
|
38535
|
+
batchOf.set(path11, Math.floor(index / FETCH_BATCH_SIZE));
|
|
38536
|
+
}
|
|
38537
|
+
const fetchedBatches = new Set;
|
|
38538
|
+
const inFlight = new Map;
|
|
38539
|
+
async function fetchAndCache(batchPaths) {
|
|
38540
|
+
const body = await deps.httpArchiveOps("/api/archive/fetch", { method: "POST", body: { paths: batchPaths, flow } });
|
|
38541
|
+
for (const u3 of body.urls)
|
|
38542
|
+
cache.set(u3.path, u3.url);
|
|
38543
|
+
}
|
|
38544
|
+
return {
|
|
38545
|
+
async getUrl(path11, opts) {
|
|
38546
|
+
if (opts?.forceRefresh) {
|
|
38547
|
+
if (process.env.DEBUG) {
|
|
38548
|
+
console.error(`[archive-gate] re-issuing expired URL for ${path11}`);
|
|
38549
|
+
}
|
|
38550
|
+
await fetchAndCache([path11]);
|
|
38551
|
+
const fresh = cache.get(path11);
|
|
38552
|
+
if (!fresh) {
|
|
38553
|
+
throw new Error(`archive gate: no URL returned for ${path11}`);
|
|
38554
|
+
}
|
|
38555
|
+
return fresh;
|
|
38556
|
+
}
|
|
38557
|
+
const batchIndex = batchOf.get(path11);
|
|
38558
|
+
if (batchIndex === undefined) {
|
|
38559
|
+
throw new Error(`archive gate: ${path11} was not in the quoted batch`);
|
|
38560
|
+
}
|
|
38561
|
+
if (!fetchedBatches.has(batchIndex)) {
|
|
38562
|
+
let inflight = inFlight.get(batchIndex);
|
|
38563
|
+
if (!inflight) {
|
|
38564
|
+
const batchPaths = paths.filter((p2) => batchOf.get(p2) === batchIndex);
|
|
38565
|
+
inflight = fetchAndCache(batchPaths).then(() => {
|
|
38566
|
+
fetchedBatches.add(batchIndex);
|
|
38567
|
+
});
|
|
38568
|
+
inFlight.set(batchIndex, inflight);
|
|
38569
|
+
}
|
|
38570
|
+
await inflight;
|
|
38571
|
+
}
|
|
38572
|
+
const url = cache.get(path11);
|
|
38573
|
+
if (!url)
|
|
38574
|
+
throw new Error(`archive gate: no URL returned for ${path11}`);
|
|
38575
|
+
return url;
|
|
38576
|
+
}
|
|
38577
|
+
};
|
|
38578
|
+
}
|
|
38579
|
+
|
|
38580
|
+
// src/commands/repair.ts
|
|
38456
38581
|
init_output();
|
|
38457
38582
|
var REPAIR_EXIT = {
|
|
38458
38583
|
OK: 0,
|
|
@@ -38502,8 +38627,8 @@ function parseHeight3(value, flag) {
|
|
|
38502
38627
|
}
|
|
38503
38628
|
return parsed;
|
|
38504
38629
|
}
|
|
38505
|
-
async function planRange(db, reference, partition) {
|
|
38506
|
-
const bytes = await fetchVerifiedPartition(reference, partition);
|
|
38630
|
+
async function planRange(db, reference, partition, gate) {
|
|
38631
|
+
const bytes = await fetchVerifiedPartition(reference, partition, gate);
|
|
38507
38632
|
const archiveBlocks = await readBlocksPartition(bytes, `${partition.from_block}-${partition.to_block}`);
|
|
38508
38633
|
const localRows = await db.selectFrom("blocks").select(["height", "hash"]).where("canonical", "=", true).where("height", ">=", partition.from_block).where("height", "<=", partition.to_block).execute();
|
|
38509
38634
|
const localByHeight = new Map(localRows.map((r2) => [Number(r2.height), r2.hash]));
|
|
@@ -38530,10 +38655,10 @@ async function planRange(db, reference, partition) {
|
|
|
38530
38655
|
const extraHeights = [...localByHeight.keys()].sort((a4, b3) => a4 - b3);
|
|
38531
38656
|
return { fixes, extraHeights };
|
|
38532
38657
|
}
|
|
38533
|
-
async function applyFixes(db, reference, partition, fixes) {
|
|
38658
|
+
async function applyFixes(db, reference, partition, fixes, gate) {
|
|
38534
38659
|
if (fixes.length === 0)
|
|
38535
38660
|
return 0;
|
|
38536
|
-
const bytes = await fetchVerifiedPartition(reference, partition);
|
|
38661
|
+
const bytes = await fetchVerifiedPartition(reference, partition, gate);
|
|
38537
38662
|
const archiveBlocks = await readBlocksPartition(bytes, `apply-${partition.from_block}`);
|
|
38538
38663
|
const byHeight = new Map(archiveBlocks.map((b3) => [b3.height, b3]));
|
|
38539
38664
|
const targets = fixes.map((fix) => byHeight.get(fix.height)).filter((b3) => b3 !== undefined);
|
|
@@ -38562,7 +38687,7 @@ async function applyFixes(db, reference, partition, fixes) {
|
|
|
38562
38687
|
return targets.length;
|
|
38563
38688
|
}
|
|
38564
38689
|
function registerRepairCommand(program2) {
|
|
38565
|
-
program2.command("repair").description("Replace local chain data that diverges from a signed archive (dry-run by default)").requiredOption("--against <manifest>", "archive manifest: an https URL or a local file path").option("--from-block <n>", "first height to consider").option("--to-block <n>", "last height to consider").option("--apply", "write the repair (default is a dry-run plan)").option("--public-key <pem>", "pin the signing key instead of fetching it").option("--json", "Output as JSON").addHelpText("after", `
|
|
38690
|
+
program2.command("repair").description("Replace local chain data that diverges from a signed archive (dry-run by default)").requiredOption("--against <manifest>", "archive manifest: an https URL or a local file path").option("--from-block <n>", "first height to consider").option("--to-block <n>", "last height to consider").option("--apply", "write the repair (default is a dry-run plan)").option("--public-key <pem>", "pin the signing key instead of fetching it").option("-y, --yes", "skip the confirmation prompt for a metered fetch").option("--json", "Output as JSON").addHelpText("after", `
|
|
38566
38691
|
Examples:
|
|
38567
38692
|
$ secondlayer repair --against ./snapshot.json # plan only
|
|
38568
38693
|
$ secondlayer repair --against ./snapshot.json --apply # write the fix
|
|
@@ -38608,12 +38733,37 @@ Exit codes:
|
|
|
38608
38733
|
process.exit(REPAIR_EXIT.OK);
|
|
38609
38734
|
}
|
|
38610
38735
|
const partitions = (reference.manifest.partitions ?? []).filter((p2) => p2.dataset === "blocks" && divergent.some((d2) => d2.from_block === p2.from_block && d2.to_block === p2.to_block));
|
|
38736
|
+
let gate;
|
|
38737
|
+
if (isOfficialArchive(reference)) {
|
|
38738
|
+
const paths = partitions.map((p2) => p2.path);
|
|
38739
|
+
const result = await quoteArchiveFetch(paths, "repair");
|
|
38740
|
+
if (!result.ok) {
|
|
38741
|
+
printError(result.kind === "not_configured" ? ARCHIVE_GATE_NOT_CONFIGURED_MESSAGE : result.message);
|
|
38742
|
+
process.exit(REPAIR_EXIT.UNANCHORED);
|
|
38743
|
+
}
|
|
38744
|
+
if (!result.quote.sufficient) {
|
|
38745
|
+
printError(formatInsufficientMessage(result.quote));
|
|
38746
|
+
process.exit(REPAIR_EXIT.UNANCHORED);
|
|
38747
|
+
}
|
|
38748
|
+
note(` metered: ${formatQuoteValue(result.quote, "repair")}`);
|
|
38749
|
+
if (shouldPromptForGatedFetch(opts)) {
|
|
38750
|
+
const proceed = await confirm7({
|
|
38751
|
+
message: `Fetch ${partitions.length} partition(s) from the archive?`,
|
|
38752
|
+
default: true
|
|
38753
|
+
});
|
|
38754
|
+
if (!proceed) {
|
|
38755
|
+
note("Nothing was fetched.");
|
|
38756
|
+
process.exit(REPAIR_EXIT.UNANCHORED);
|
|
38757
|
+
}
|
|
38758
|
+
}
|
|
38759
|
+
gate = createGatedFetcher(paths, "repair");
|
|
38760
|
+
}
|
|
38611
38761
|
const allFixes = [];
|
|
38612
38762
|
const allExtra = [];
|
|
38613
38763
|
const planned = [];
|
|
38614
38764
|
for (const partition of partitions) {
|
|
38615
38765
|
note(` reading ${partition.dataset} ${partition.from_block}-${partition.to_block}`);
|
|
38616
|
-
const { fixes, extraHeights } = await planRange(db, reference, partition);
|
|
38766
|
+
const { fixes, extraHeights } = await planRange(db, reference, partition, gate);
|
|
38617
38767
|
planned.push({ partition, fixes });
|
|
38618
38768
|
allFixes.push(...fixes);
|
|
38619
38769
|
allExtra.push(...extraHeights);
|
|
@@ -38621,7 +38771,7 @@ Exit codes:
|
|
|
38621
38771
|
let applied = 0;
|
|
38622
38772
|
if (opts.apply) {
|
|
38623
38773
|
for (const { partition, fixes } of planned) {
|
|
38624
|
-
applied += await applyFixes(db, reference, partition, fixes);
|
|
38774
|
+
applied += await applyFixes(db, reference, partition, fixes, gate);
|
|
38625
38775
|
}
|
|
38626
38776
|
}
|
|
38627
38777
|
let remaining = divergent.length;
|
|
@@ -38698,7 +38848,7 @@ import { unlink as unlink3, writeFile as writeFile6 } from "node:fs/promises";
|
|
|
38698
38848
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
38699
38849
|
import { join as join16 } from "node:path";
|
|
38700
38850
|
import { ParquetReader as ParquetReader2 } from "@dsnp/parquetjs";
|
|
38701
|
-
import { confirm as
|
|
38851
|
+
import { confirm as confirm8 } from "@inquirer/prompts";
|
|
38702
38852
|
import {
|
|
38703
38853
|
copyStatement,
|
|
38704
38854
|
writeRowsToCopyStream
|
|
@@ -38794,8 +38944,8 @@ async function* readPartitionRows(bytes, label) {
|
|
|
38794
38944
|
await unlink3(path11).catch(() => {});
|
|
38795
38945
|
}
|
|
38796
38946
|
}
|
|
38797
|
-
async function loadPartition(reference, partition, rawClient) {
|
|
38798
|
-
const bytes = await fetchVerifiedPartition(reference, partition);
|
|
38947
|
+
async function loadPartition(reference, partition, rawClient, gate) {
|
|
38948
|
+
const bytes = await fetchVerifiedPartition(reference, partition, gate);
|
|
38799
38949
|
const writable2 = await rawClient.unsafe(copyStatement(partition.dataset)).writable();
|
|
38800
38950
|
const written = await writeRowsToCopyStream({
|
|
38801
38951
|
writable: writable2,
|
|
@@ -38872,6 +39022,23 @@ Exit codes:
|
|
|
38872
39022
|
const tipHeight = Math.max(...partitions.map((p2) => p2.to_block));
|
|
38873
39023
|
const restoreSeconds = totalRows / RESTORE_ROWS_PER_SECOND;
|
|
38874
39024
|
const genesisSeconds = tipHeight / BLOCKS_PER_DAY_ESTIMATE * 86400;
|
|
39025
|
+
const gated = isOfficialArchive(reference);
|
|
39026
|
+
let quoteLine;
|
|
39027
|
+
let gate;
|
|
39028
|
+
if (gated) {
|
|
39029
|
+
const paths = partitions.map((p2) => p2.path);
|
|
39030
|
+
const result = await quoteArchiveFetch(paths, "bootstrap");
|
|
39031
|
+
if (!result.ok) {
|
|
39032
|
+
printError(result.kind === "not_configured" ? ARCHIVE_GATE_NOT_CONFIGURED_MESSAGE : result.message);
|
|
39033
|
+
process.exit(BOOTSTRAP_EXIT.REFUSED);
|
|
39034
|
+
}
|
|
39035
|
+
if (!result.quote.sufficient) {
|
|
39036
|
+
printError(formatInsufficientMessage(result.quote));
|
|
39037
|
+
process.exit(BOOTSTRAP_EXIT.REFUSED);
|
|
39038
|
+
}
|
|
39039
|
+
quoteLine = formatQuoteValue(result.quote, "bootstrap");
|
|
39040
|
+
gate = createGatedFetcher(paths, "bootstrap");
|
|
39041
|
+
}
|
|
38875
39042
|
if (!opts.json) {
|
|
38876
39043
|
console.error("");
|
|
38877
39044
|
console.error(formatKeyValue([
|
|
@@ -38882,15 +39049,16 @@ Exit codes:
|
|
|
38882
39049
|
],
|
|
38883
39050
|
["rows", totalRows.toLocaleString()],
|
|
38884
39051
|
["download", `${(totalBytes / 1e9).toFixed(1)} GB`],
|
|
38885
|
-
["signature", "verified"]
|
|
39052
|
+
["signature", "verified"],
|
|
39053
|
+
...quoteLine ? [["metered", quoteLine]] : []
|
|
38886
39054
|
]));
|
|
38887
39055
|
console.error("");
|
|
38888
39056
|
console.error(` sync from genesis ${bold(`~${formatDuration2(genesisSeconds)}`)} ${dim("(estimate)")}`);
|
|
38889
39057
|
console.error(` restore from archive ${bold(`~${formatDuration2(restoreSeconds)}`)}`);
|
|
38890
39058
|
console.error("");
|
|
38891
39059
|
}
|
|
38892
|
-
if (
|
|
38893
|
-
const proceed = await
|
|
39060
|
+
if (shouldPromptForGatedFetch(opts)) {
|
|
39061
|
+
const proceed = await confirm8({
|
|
38894
39062
|
message: "Restore from the verified archive?",
|
|
38895
39063
|
default: true
|
|
38896
39064
|
});
|
|
@@ -38907,7 +39075,7 @@ Exit codes:
|
|
|
38907
39075
|
for (const dataset of order) {
|
|
38908
39076
|
const datasetPartitions = partitions.filter((p2) => p2.dataset === dataset).sort((a4, b3) => a4.from_block - b3.from_block);
|
|
38909
39077
|
for (const [index, partition] of datasetPartitions.entries()) {
|
|
38910
|
-
loaded[dataset] += await loadPartition(reference, partition, rawClient);
|
|
39078
|
+
loaded[dataset] += await loadPartition(reference, partition, rawClient, gate);
|
|
38911
39079
|
const done = index + 1;
|
|
38912
39080
|
if (done % 10 === 0 || done === datasetPartitions.length) {
|
|
38913
39081
|
const elapsed2 = (Date.now() - startedAt) / 1000;
|
|
@@ -38964,7 +39132,8 @@ Exit codes:
|
|
|
38964
39132
|
elapsed_seconds: Math.round(elapsed),
|
|
38965
39133
|
resume_from: tipHeight + 1,
|
|
38966
39134
|
node_tip_at_start: nodeTipAtStart,
|
|
38967
|
-
catch_up_blocks: seam?.gap ?? null
|
|
39135
|
+
catch_up_blocks: seam?.gap ?? null,
|
|
39136
|
+
metered: quoteLine ?? null
|
|
38968
39137
|
};
|
|
38969
39138
|
output({
|
|
38970
39139
|
json: opts.json,
|
|
@@ -39006,19 +39175,91 @@ Exit codes:
|
|
|
39006
39175
|
function registerBootstrapCommand(program2) {
|
|
39007
39176
|
attachBootstrapCommand(program2.command("bootstrap").description("Restore chain history from a verified archive instead of syncing from genesis"));
|
|
39008
39177
|
}
|
|
39178
|
+
// src/commands/uninstall.ts
|
|
39179
|
+
init_output();
|
|
39180
|
+
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
39181
|
+
import { existsSync as existsSync8, readFileSync as readFileSync7 } from "node:fs";
|
|
39182
|
+
import { join as join17, resolve as resolve13 } from "node:path";
|
|
39183
|
+
import { planUninstall, uninstallCommand } from "@secondlayer/shared/runtime";
|
|
39184
|
+
var UNINSTALL_EXIT = { OK: 0, FAILED: 1, REFUSED: 2 };
|
|
39185
|
+
var DEFAULT_COMPOSE = "docker/oss/docker-compose.yml";
|
|
39186
|
+
function bundleHasKeys(bundleDir) {
|
|
39187
|
+
const manifest = join17(bundleDir, "manifest.json");
|
|
39188
|
+
if (!existsSync8(manifest))
|
|
39189
|
+
return false;
|
|
39190
|
+
try {
|
|
39191
|
+
const parsed = JSON.parse(readFileSync7(manifest, "utf8"));
|
|
39192
|
+
return parsed.secrets != null;
|
|
39193
|
+
} catch {
|
|
39194
|
+
return false;
|
|
39195
|
+
}
|
|
39196
|
+
}
|
|
39197
|
+
function registerUninstallCommand(program2) {
|
|
39198
|
+
program2.command("uninstall").description("Stop the stack; the index, chainstate, and keys survive").option("--compose <file>", "compose file to tear down", DEFAULT_COMPOSE).option("--purge", "also destroy the volumes (wipes the index)").option("--yes", "confirm a purge").option("--backup <dir>", "a bundle proving the keys exist elsewhere (required to purge)").option("--apply", "actually run it (default is a dry run)").option("--json", "Output as JSON").action(async (opts) => {
|
|
39199
|
+
const dataDir = process.env.DATA_DIR ?? "./data";
|
|
39200
|
+
const secretsPresent = !!process.env.SECONDLAYER_SECRETS_KEY || existsSync8(resolve13(process.cwd(), ".env.local"));
|
|
39201
|
+
const keysBackedUp = opts.backup ? bundleHasKeys(resolve13(opts.backup)) : false;
|
|
39202
|
+
const decision = planUninstall({
|
|
39203
|
+
purge: opts.purge === true,
|
|
39204
|
+
confirmed: opts.yes === true,
|
|
39205
|
+
keysBackedUp,
|
|
39206
|
+
secretsPresent,
|
|
39207
|
+
dataDir
|
|
39208
|
+
});
|
|
39209
|
+
if (!decision.ok) {
|
|
39210
|
+
printError(decision.reason);
|
|
39211
|
+
process.exit(UNINSTALL_EXIT.REFUSED);
|
|
39212
|
+
}
|
|
39213
|
+
const cmd = uninstallCommand(decision.plan, opts.compose);
|
|
39214
|
+
if (!opts.apply) {
|
|
39215
|
+
output({
|
|
39216
|
+
json: opts.json,
|
|
39217
|
+
data: { dryRun: true, plan: decision.plan, command: cmd },
|
|
39218
|
+
human: () => {
|
|
39219
|
+
note("dry run — nothing removed. Pass --apply to proceed.");
|
|
39220
|
+
note(`would run: docker ${cmd.join(" ")}`);
|
|
39221
|
+
for (const p2 of decision.plan.preserves) {
|
|
39222
|
+
note(`preserved · ${p2.what}: ${p2.detail}`);
|
|
39223
|
+
}
|
|
39224
|
+
for (const d2 of decision.plan.destroys) {
|
|
39225
|
+
warn(`DESTROYED · volume ${d2}`);
|
|
39226
|
+
}
|
|
39227
|
+
}
|
|
39228
|
+
});
|
|
39229
|
+
return;
|
|
39230
|
+
}
|
|
39231
|
+
const res = spawnSync3("docker", cmd, { stdio: "inherit" });
|
|
39232
|
+
if ((res.status ?? 1) !== 0) {
|
|
39233
|
+
printError(`docker ${cmd.join(" ")} exited ${res.status}`);
|
|
39234
|
+
process.exit(UNINSTALL_EXIT.FAILED);
|
|
39235
|
+
}
|
|
39236
|
+
output({
|
|
39237
|
+
json: opts.json,
|
|
39238
|
+
data: { removed: true, plan: decision.plan },
|
|
39239
|
+
human: () => {
|
|
39240
|
+
success("stack removed");
|
|
39241
|
+
for (const w2 of decision.warnings)
|
|
39242
|
+
warn(w2);
|
|
39243
|
+
for (const p2 of decision.plan.preserves) {
|
|
39244
|
+
note(`preserved · ${p2.what}: ${p2.detail}`);
|
|
39245
|
+
}
|
|
39246
|
+
}
|
|
39247
|
+
});
|
|
39248
|
+
});
|
|
39249
|
+
}
|
|
39009
39250
|
// src/commands/backup.ts
|
|
39010
39251
|
init_output();
|
|
39011
39252
|
import { createHash as createHash4 } from "node:crypto";
|
|
39012
39253
|
import {
|
|
39013
39254
|
chmodSync,
|
|
39014
|
-
existsSync as
|
|
39255
|
+
existsSync as existsSync9,
|
|
39015
39256
|
mkdirSync as mkdirSync6,
|
|
39016
|
-
readFileSync as
|
|
39257
|
+
readFileSync as readFileSync8,
|
|
39017
39258
|
statSync as statSync3,
|
|
39018
39259
|
writeFileSync as writeFileSync7
|
|
39019
39260
|
} from "node:fs";
|
|
39020
|
-
import { join as
|
|
39021
|
-
import { getDb as getDb5, sql as sql4 } from "@secondlayer/shared/db";
|
|
39261
|
+
import { join as join18, resolve as resolve14 } from "node:path";
|
|
39262
|
+
import { closeDb, getDb as getDb5, sql as sql4 } from "@secondlayer/shared/db";
|
|
39022
39263
|
import {
|
|
39023
39264
|
decryptBundle,
|
|
39024
39265
|
encryptBundle,
|
|
@@ -39044,7 +39285,7 @@ function readSecretsKey() {
|
|
|
39044
39285
|
return key ? key : undefined;
|
|
39045
39286
|
}
|
|
39046
39287
|
function sha256File(path11) {
|
|
39047
|
-
return createHash4("sha256").update(
|
|
39288
|
+
return createHash4("sha256").update(readFileSync8(path11)).digest("hex");
|
|
39048
39289
|
}
|
|
39049
39290
|
async function readScope() {
|
|
39050
39291
|
const db = getDb5();
|
|
@@ -39076,6 +39317,7 @@ function attachBackupCommand(cmd) {
|
|
|
39076
39317
|
return cmd.requiredOption("--out <dir>", "directory to write the bundle into").option("--passphrase <passphrase>", "encrypt bundled secrets (or set SECONDLAYER_BACKUP_PASSPHRASE)").option("--no-secrets", "omit keys; the bundle restores data only").option("--json", "Output as JSON").action(async (opts) => {
|
|
39077
39318
|
try {
|
|
39078
39319
|
await runBackup(opts);
|
|
39320
|
+
await closeDb();
|
|
39079
39321
|
} catch (error2) {
|
|
39080
39322
|
printError(error2 instanceof Error ? error2.message : String(error2));
|
|
39081
39323
|
process.exit(BACKUP_EXIT.FAILED);
|
|
@@ -39104,10 +39346,10 @@ async function runBackup(opts) {
|
|
|
39104
39346
|
printError(plan.reason);
|
|
39105
39347
|
process.exit(BACKUP_EXIT.REFUSED);
|
|
39106
39348
|
}
|
|
39107
|
-
const outDir =
|
|
39349
|
+
const outDir = resolve14(opts.out);
|
|
39108
39350
|
mkdirSync6(outDir, { recursive: true });
|
|
39109
39351
|
console.error("dumping database…");
|
|
39110
|
-
const dumpPath =
|
|
39352
|
+
const dumpPath = join18(outDir, DB_FILE);
|
|
39111
39353
|
const dump = await runProcess(["pg_dump", "-Fc", "--no-owner", "--no-acl", "-f", dumpPath, databaseUrl], "pg_dump");
|
|
39112
39354
|
if (!dump.ok) {
|
|
39113
39355
|
printError(dump.stderr);
|
|
@@ -39126,7 +39368,7 @@ async function runBackup(opts) {
|
|
|
39126
39368
|
const lines2 = SECRET_ENV_KEYS.filter((k2) => process.env[k2]).map((k2) => `${k2}=${process.env[k2]}`);
|
|
39127
39369
|
const sealed = encryptBundle(lines2.join(`
|
|
39128
39370
|
`), passphrase);
|
|
39129
|
-
const secretsPath =
|
|
39371
|
+
const secretsPath = join18(outDir, SECRETS_FILE);
|
|
39130
39372
|
writeFileSync7(secretsPath, sealed);
|
|
39131
39373
|
chmodSync(secretsPath, 384);
|
|
39132
39374
|
manifest.secrets = {
|
|
@@ -39135,7 +39377,7 @@ async function runBackup(opts) {
|
|
|
39135
39377
|
canary: sealKeyCanary(secretsKey)
|
|
39136
39378
|
};
|
|
39137
39379
|
}
|
|
39138
|
-
writeFileSync7(
|
|
39380
|
+
writeFileSync7(join18(outDir, MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}
|
|
39139
39381
|
`);
|
|
39140
39382
|
output({
|
|
39141
39383
|
json: opts.json,
|
|
@@ -39155,6 +39397,7 @@ function attachRestoreCommand(cmd) {
|
|
|
39155
39397
|
return cmd.requiredOption("--from <dir>", "bundle directory to restore from").option("--passphrase <passphrase>", "decrypt bundled secrets (or set SECONDLAYER_BACKUP_PASSPHRASE)").option("--apply", "actually restore (default is a dry run)").option("--force", "restore over a database that already holds chain data").option("--json", "Output as JSON").action(async (opts) => {
|
|
39156
39398
|
try {
|
|
39157
39399
|
await runRestore(opts);
|
|
39400
|
+
await closeDb();
|
|
39158
39401
|
} catch (error2) {
|
|
39159
39402
|
printError(error2 instanceof Error ? error2.message : String(error2));
|
|
39160
39403
|
process.exit(BACKUP_EXIT.FAILED);
|
|
@@ -39163,14 +39406,14 @@ function attachRestoreCommand(cmd) {
|
|
|
39163
39406
|
}
|
|
39164
39407
|
async function runRestore(opts) {
|
|
39165
39408
|
const databaseUrl = requireDatabaseUrl();
|
|
39166
|
-
const dir =
|
|
39167
|
-
const manifestPath =
|
|
39168
|
-
if (!
|
|
39409
|
+
const dir = resolve14(opts.from);
|
|
39410
|
+
const manifestPath = join18(dir, MANIFEST_FILE);
|
|
39411
|
+
if (!existsSync9(manifestPath)) {
|
|
39169
39412
|
printError(`no ${MANIFEST_FILE} in ${dir}`);
|
|
39170
39413
|
process.exit(BACKUP_EXIT.REFUSED);
|
|
39171
39414
|
}
|
|
39172
|
-
const manifest = JSON.parse(
|
|
39173
|
-
const dumpPath =
|
|
39415
|
+
const manifest = JSON.parse(readFileSync8(manifestPath, "utf8"));
|
|
39416
|
+
const dumpPath = join18(dir, manifest.db?.file ?? DB_FILE);
|
|
39174
39417
|
if (manifest.db && sha256File(dumpPath) !== manifest.db.sha256) {
|
|
39175
39418
|
printError("the database dump does not match the digest in the manifest; this bundle is corrupt or truncated");
|
|
39176
39419
|
process.exit(BACKUP_EXIT.REFUSED);
|
|
@@ -39184,7 +39427,7 @@ async function runRestore(opts) {
|
|
|
39184
39427
|
process.exit(BACKUP_EXIT.REFUSED);
|
|
39185
39428
|
}
|
|
39186
39429
|
try {
|
|
39187
|
-
const env2 = decryptBundle(
|
|
39430
|
+
const env2 = decryptBundle(readFileSync8(join18(dir, manifest.secrets.file)), passphrase);
|
|
39188
39431
|
bundleKey = env2.split(`
|
|
39189
39432
|
`).find((l2) => l2.startsWith("SECONDLAYER_SECRETS_KEY="))?.slice("SECONDLAYER_SECRETS_KEY=".length).trim();
|
|
39190
39433
|
} catch {
|
|
@@ -39263,8 +39506,8 @@ function registerRestoreCommand(program2) {
|
|
|
39263
39506
|
}
|
|
39264
39507
|
// src/lib/instance-init.ts
|
|
39265
39508
|
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
39266
|
-
import { existsSync as
|
|
39267
|
-
import { resolve as
|
|
39509
|
+
import { existsSync as existsSync10, readFileSync as readFileSync9, writeFileSync as writeFileSync8 } from "node:fs";
|
|
39510
|
+
import { resolve as resolve15 } from "node:path";
|
|
39268
39511
|
import { generateEd25519KeyPair } from "@secondlayer/shared/crypto/ed25519";
|
|
39269
39512
|
import { INSTANCE_NETWORKS } from "@secondlayer/shared/db/queries/instance";
|
|
39270
39513
|
var INSTANCE_ENV_FILE = ".env.local";
|
|
@@ -39298,9 +39541,9 @@ function unescapeEnvValue(raw) {
|
|
|
39298
39541
|
`).replace(/\\"/g, '"').replace(/\\\\/g, "\\");
|
|
39299
39542
|
}
|
|
39300
39543
|
function readEnvValue(envPath, key) {
|
|
39301
|
-
if (!
|
|
39544
|
+
if (!existsSync10(envPath))
|
|
39302
39545
|
return null;
|
|
39303
|
-
const match =
|
|
39546
|
+
const match = readFileSync9(envPath, "utf8").match(new RegExp(`^${key}=(.*)$`, "m"));
|
|
39304
39547
|
return match ? unescapeEnvValue(match[1]) : null;
|
|
39305
39548
|
}
|
|
39306
39549
|
function buildInstanceEnv(input4) {
|
|
@@ -39343,12 +39586,12 @@ function renderInstanceEnv(env2) {
|
|
|
39343
39586
|
`);
|
|
39344
39587
|
}
|
|
39345
39588
|
function writeInstanceEnv(cwd, env2, file = INSTANCE_ENV_FILE) {
|
|
39346
|
-
const path11 =
|
|
39589
|
+
const path11 = resolve15(cwd, file);
|
|
39347
39590
|
writeFileSync8(path11, renderInstanceEnv(env2), { mode: 384 });
|
|
39348
39591
|
return path11;
|
|
39349
39592
|
}
|
|
39350
39593
|
function loadExistingInstanceEnv(cwd, file = INSTANCE_ENV_FILE) {
|
|
39351
|
-
const path11 =
|
|
39594
|
+
const path11 = resolve15(cwd, file);
|
|
39352
39595
|
return {
|
|
39353
39596
|
INSTANCE_TOKEN: readEnvValue(path11, "INSTANCE_TOKEN"),
|
|
39354
39597
|
SECONDLAYER_SECRETS_KEY: readEnvValue(path11, "SECONDLAYER_SECRETS_KEY"),
|
|
@@ -39502,7 +39745,7 @@ function registerStartCommand(program2) {
|
|
|
39502
39745
|
}
|
|
39503
39746
|
// src/cli.ts
|
|
39504
39747
|
var { version } = package_default;
|
|
39505
|
-
program.name("secondlayer").alias("sl").description("Secondlayer CLI —
|
|
39748
|
+
program.name("secondlayer").alias("sl").description("Secondlayer CLI — run a Stacks index on your own hardware").version(version).option("--network <network>", "Override network (local, testnet, mainnet)").option("--api-key <key>", "API credential (overrides SL_API_KEY)").option("--api-url <url>", "API endpoint (overrides SL_API_URL)").showSuggestionAfterError(true).showHelpAfterError("(run `secondlayer --help` to see available commands)");
|
|
39506
39749
|
program.hook("preAction", (thisCommand) => {
|
|
39507
39750
|
const { network, apiKey, apiUrl } = thisCommand.opts();
|
|
39508
39751
|
if (network)
|
|
@@ -39516,7 +39759,7 @@ program.addHelpText("after", `
|
|
|
39516
39759
|
Quickstart:
|
|
39517
39760
|
$ secondlayer init --network mainnet
|
|
39518
39761
|
$ secondlayer bootstrap --against <manifest>
|
|
39519
|
-
$ secondlayer subgraphs create my-watcher --
|
|
39762
|
+
$ secondlayer subgraphs create my-watcher --from-contract SP....my-contract
|
|
39520
39763
|
$ secondlayer subgraphs deploy subgraphs/my-watcher.ts
|
|
39521
39764
|
`);
|
|
39522
39765
|
program.commandsGroup("Getting started:");
|
|
@@ -39528,7 +39771,7 @@ registerConsoleCommand(program);
|
|
|
39528
39771
|
registerLoginCommand(program);
|
|
39529
39772
|
registerLogoutCommand(program);
|
|
39530
39773
|
registerWhoamiCommand(program);
|
|
39531
|
-
program.commandsGroup("
|
|
39774
|
+
program.commandsGroup("Your data:");
|
|
39532
39775
|
registerSubgraphsCommand(program);
|
|
39533
39776
|
registerSubscriptionsCommand(program);
|
|
39534
39777
|
registerStreamsCommand(program);
|
|
@@ -39560,9 +39803,10 @@ registerVerifyCommand(program);
|
|
|
39560
39803
|
registerRepairCommand(program);
|
|
39561
39804
|
registerBackupCommand(program);
|
|
39562
39805
|
registerRestoreCommand(program);
|
|
39563
|
-
program
|
|
39806
|
+
registerUninstallCommand(program);
|
|
39807
|
+
program.commandsGroup("Archive:");
|
|
39564
39808
|
registerCreditsCommand(program);
|
|
39565
39809
|
program.parse();
|
|
39566
39810
|
|
|
39567
|
-
//# debugId=
|
|
39811
|
+
//# debugId=5BF91F30BB4466C964756E2164756E21
|
|
39568
39812
|
//# sourceMappingURL=cli.js.map
|