@secondlayer/cli 9.9.0 → 10.0.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 +4 -2
- package/dist/cli.js +533 -253
- package/dist/cli.js.map +28 -24
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -2481,6 +2481,25 @@ var require_commander = __commonJS((exports) => {
|
|
|
2481
2481
|
exports.InvalidOptionArgumentError = InvalidArgumentError;
|
|
2482
2482
|
});
|
|
2483
2483
|
|
|
2484
|
+
// ../../node_modules/commander/esm.mjs
|
|
2485
|
+
var import__, program, createCommand, createArgument, createOption, CommanderError, InvalidArgumentError, InvalidOptionArgumentError, Command, Argument, Option, Help;
|
|
2486
|
+
var init_esm = __esm(() => {
|
|
2487
|
+
import__ = __toESM(require_commander(), 1);
|
|
2488
|
+
({
|
|
2489
|
+
program,
|
|
2490
|
+
createCommand,
|
|
2491
|
+
createArgument,
|
|
2492
|
+
createOption,
|
|
2493
|
+
CommanderError,
|
|
2494
|
+
InvalidArgumentError,
|
|
2495
|
+
InvalidOptionArgumentError,
|
|
2496
|
+
Command,
|
|
2497
|
+
Argument,
|
|
2498
|
+
Option,
|
|
2499
|
+
Help
|
|
2500
|
+
} = import__.default);
|
|
2501
|
+
});
|
|
2502
|
+
|
|
2484
2503
|
// src/lib/session.ts
|
|
2485
2504
|
import {
|
|
2486
2505
|
chmod,
|
|
@@ -2543,6 +2562,9 @@ async function resolveAuth() {
|
|
|
2543
2562
|
if (envKey) {
|
|
2544
2563
|
return { apiUrl, ephemeralKey: envKey, fromEnv: true };
|
|
2545
2564
|
}
|
|
2565
|
+
if (isOssMode()) {
|
|
2566
|
+
return { apiUrl, ephemeralKey: "", fromEnv: true };
|
|
2567
|
+
}
|
|
2546
2568
|
const session = await readSession();
|
|
2547
2569
|
if (!session) {
|
|
2548
2570
|
const err = new Error("Not logged in — run `sl login`");
|
|
@@ -2551,6 +2573,9 @@ async function resolveAuth() {
|
|
|
2551
2573
|
}
|
|
2552
2574
|
return { apiUrl, ephemeralKey: session.token, fromEnv: false };
|
|
2553
2575
|
}
|
|
2576
|
+
function isOssMode() {
|
|
2577
|
+
return !!process.env.SL_API_URL;
|
|
2578
|
+
}
|
|
2554
2579
|
var DEFAULT_API_URL = "https://api.secondlayer.tools";
|
|
2555
2580
|
var init_resolve_auth = __esm(() => {
|
|
2556
2581
|
init_session();
|
|
@@ -2590,7 +2615,7 @@ async function httpPlatform(path2, opts = {}) {
|
|
|
2590
2615
|
try {
|
|
2591
2616
|
auth = await resolveAuth();
|
|
2592
2617
|
} catch {
|
|
2593
|
-
throw new CliHttpError(401, "SESSION_EXPIRED", { error: "Not logged in" }, "Not logged in — run `sl login`");
|
|
2618
|
+
throw new CliHttpError(401, "SESSION_EXPIRED", { error: "Not logged in" }, isOssMode() ? "Not authenticated — set SL_API_KEY from `sl init`" : "Not logged in — run `sl login`");
|
|
2594
2619
|
}
|
|
2595
2620
|
return request(`${auth.apiUrl}${path2}`, {
|
|
2596
2621
|
...opts,
|
|
@@ -2745,7 +2770,7 @@ function handleApiError(err, action) {
|
|
|
2745
2770
|
const status = err instanceof ApiError || err instanceof CliHttpError ? err.status : undefined;
|
|
2746
2771
|
if (err instanceof CliHttpError && err.code === "SESSION_EXPIRED" || status === 401) {
|
|
2747
2772
|
printError("Authentication required.", {
|
|
2748
|
-
hint: "Run `sl login` to re-authenticate."
|
|
2773
|
+
hint: isOssMode() ? "Set SL_API_KEY / INSTANCE_TOKEN from `sl init`." : "Run `sl login` to re-authenticate."
|
|
2749
2774
|
});
|
|
2750
2775
|
process.exit(1);
|
|
2751
2776
|
}
|
|
@@ -28826,7 +28851,7 @@ async function runLoginFlow(options3 = {}) {
|
|
|
28826
28851
|
}
|
|
28827
28852
|
}
|
|
28828
28853
|
function registerLoginCommand(program2) {
|
|
28829
|
-
program2.command("login").description("Log in to Secondlayer (magic-link email)").option("--force", "Skip the already-logged-in check and re-run the flow").option("--with-token", "Read an API key from stdin (non-interactive)").addHelpText("after", `
|
|
28854
|
+
program2.command("login", { hidden: true }).description("Log in to Secondlayer (magic-link email)").option("--force", "Skip the already-logged-in check and re-run the flow").option("--with-token", "Read an API key from stdin (non-interactive)").addHelpText("after", `
|
|
28830
28855
|
Examples:
|
|
28831
28856
|
$ sl login
|
|
28832
28857
|
$ echo "$SL_API_KEY" | sl login --with-token`).action((opts) => opts.withToken ? runTokenLogin() : runLoginFlow({ force: opts.force }));
|
|
@@ -28839,6 +28864,8 @@ var init_login = __esm(() => {
|
|
|
28839
28864
|
|
|
28840
28865
|
// src/lib/require-auth.ts
|
|
28841
28866
|
async function requireAuth() {
|
|
28867
|
+
if (isOssMode())
|
|
28868
|
+
return;
|
|
28842
28869
|
const session = await readSession();
|
|
28843
28870
|
if (session)
|
|
28844
28871
|
return;
|
|
@@ -28848,6 +28875,7 @@ async function requireAuth() {
|
|
|
28848
28875
|
var init_require_auth = __esm(() => {
|
|
28849
28876
|
init_login();
|
|
28850
28877
|
init_output();
|
|
28878
|
+
init_resolve_auth();
|
|
28851
28879
|
init_session();
|
|
28852
28880
|
});
|
|
28853
28881
|
|
|
@@ -30104,7 +30132,7 @@ Stopped watching.`);
|
|
|
30104
30132
|
});
|
|
30105
30133
|
await new Promise(() => {});
|
|
30106
30134
|
});
|
|
30107
|
-
subgraphs.command("deploy <file>").alias("update").description("Deploy a subgraph definition file (alias: update — deploy is create-or-update)").option("--start-block <n>", "Override the subgraph definition startBlock for this deploy").option("--tip-first", "Go live at chain tip immediately; history backfills in the background (requires order-tolerant handlers — commutative or insert-only writes)").option("--dry-run", "Validate and preview deploy without writing changes").option("-y, --yes", "Skip the reindex confirmation prompt").option("--database-url <url>", "BYO data plane: write the subgraph's schema/rows to your own Postgres. With --dry-run, prints the DDL + grant script and verifies the connection.").option("--strict", "Run `tsc --noEmit` against the handler before deploy (slower; catches TS type errors)").
|
|
30135
|
+
subgraphs.command("deploy <file>").alias("update").description("Deploy a subgraph definition file (alias: update — deploy is create-or-update)").option("--start-block <n>", "Override the subgraph definition startBlock for this deploy").option("--tip-first", "Go live at chain tip immediately; history backfills in the background (requires order-tolerant handlers — commutative or insert-only writes)").option("--dry-run", "Validate and preview deploy without writing changes").option("-y, --yes", "Skip the reindex confirmation prompt").option("--database-url <url>", "BYO data plane: write the subgraph's schema/rows to your own Postgres. With --dry-run, prints the DDL + grant script and verifies the connection.").option("--strict", "Run `tsc --noEmit` against the handler before deploy (slower; catches TS type errors)").addOption(new Option("--visibility <visibility>", "Hosted only. Public/private namespace.").hideHelp()).option("--allow-uncommitted", "Deploy even though the source file is not committed to git (the deployed definition will exist only in the database)").action(async (file, options3) => {
|
|
30108
30136
|
try {
|
|
30109
30137
|
const absPath = resolve3(file);
|
|
30110
30138
|
if (!options3.dryRun) {
|
|
@@ -30132,7 +30160,7 @@ Stopped watching.`);
|
|
|
30132
30160
|
}
|
|
30133
30161
|
}
|
|
30134
30162
|
const config = await loadConfig2();
|
|
30135
|
-
if (config.network !== "local") {
|
|
30163
|
+
if (config.network !== "local" && !isOssMode()) {
|
|
30136
30164
|
await requireAuth();
|
|
30137
30165
|
}
|
|
30138
30166
|
const dryRun = options3.dryRun;
|
|
@@ -30666,7 +30694,7 @@ ${rows.length} row(s)`));
|
|
|
30666
30694
|
handleApiError(err, "delete subgraph");
|
|
30667
30695
|
}
|
|
30668
30696
|
});
|
|
30669
|
-
subgraphs.command("publish <name>").description("Make a subgraph publicly readable on /v1/subgraphs/<name> (claims the name globally, no key needed to read)").action(async (name) => {
|
|
30697
|
+
subgraphs.command("publish <name>", { hidden: true }).description("Make a subgraph publicly readable on /v1/subgraphs/<name> (claims the name globally, no key needed to read)").action(async (name) => {
|
|
30670
30698
|
try {
|
|
30671
30699
|
await requireAuth();
|
|
30672
30700
|
const result = await publishSubgraphApi(name);
|
|
@@ -30684,7 +30712,7 @@ ${rows.length} row(s)`));
|
|
|
30684
30712
|
handleApiError(err, "publish subgraph");
|
|
30685
30713
|
}
|
|
30686
30714
|
});
|
|
30687
|
-
subgraphs.command("unpublish <name>").description("Make a subgraph private again — reads require your API key, the public name claim is released").action(async (name) => {
|
|
30715
|
+
subgraphs.command("unpublish <name>", { hidden: true }).description("Make a subgraph private again — reads require your API key, the public name claim is released").action(async (name) => {
|
|
30688
30716
|
try {
|
|
30689
30717
|
await requireAuth();
|
|
30690
30718
|
await unpublishSubgraphApi(name);
|
|
@@ -30839,6 +30867,7 @@ async function runSubgraphSchemaCodegen(file, options3, optionSource, defaultTar
|
|
|
30839
30867
|
}
|
|
30840
30868
|
var ORM_ONLY_CODEGEN_FLAGS;
|
|
30841
30869
|
var init_subgraphs2 = __esm(() => {
|
|
30870
|
+
init_esm();
|
|
30842
30871
|
init_subgraph_scaffold();
|
|
30843
30872
|
init_subgraphs();
|
|
30844
30873
|
init_api_client();
|
|
@@ -30850,6 +30879,7 @@ var init_subgraphs2 = __esm(() => {
|
|
|
30850
30879
|
init_output();
|
|
30851
30880
|
init_require_auth();
|
|
30852
30881
|
init_resolve_auth();
|
|
30882
|
+
init_resolve_auth();
|
|
30853
30883
|
init_clarity();
|
|
30854
30884
|
init_subgraph();
|
|
30855
30885
|
init_api();
|
|
@@ -33160,12 +33190,12 @@ __export(exports_dev_state, {
|
|
|
33160
33190
|
clearDevState: () => clearDevState
|
|
33161
33191
|
});
|
|
33162
33192
|
import { homedir as homedir4 } from "node:os";
|
|
33163
|
-
import { join as
|
|
33193
|
+
import { join as join11 } from "node:path";
|
|
33164
33194
|
function getLogsDir() {
|
|
33165
33195
|
return LOGS_DIR;
|
|
33166
33196
|
}
|
|
33167
33197
|
function getLogFile(service) {
|
|
33168
|
-
return
|
|
33198
|
+
return join11(LOGS_DIR, `${service}.log`);
|
|
33169
33199
|
}
|
|
33170
33200
|
async function ensureDirs() {
|
|
33171
33201
|
await Bun.$`mkdir -p ${STATE_DIR}`.quiet();
|
|
@@ -33222,9 +33252,9 @@ async function isDevRunning() {
|
|
|
33222
33252
|
}
|
|
33223
33253
|
var STATE_DIR, DEV_STATE_PATH, LOGS_DIR;
|
|
33224
33254
|
var init_dev_state = __esm(() => {
|
|
33225
|
-
STATE_DIR =
|
|
33226
|
-
DEV_STATE_PATH =
|
|
33227
|
-
LOGS_DIR =
|
|
33255
|
+
STATE_DIR = join11(homedir4(), ".secondlayer");
|
|
33256
|
+
DEV_STATE_PATH = join11(STATE_DIR, "dev.json");
|
|
33257
|
+
LOGS_DIR = join11(STATE_DIR, "logs");
|
|
33228
33258
|
});
|
|
33229
33259
|
|
|
33230
33260
|
// src/lib/network.ts
|
|
@@ -33708,7 +33738,7 @@ __export(exports_dev_impl, {
|
|
|
33708
33738
|
isDevAlreadyRunning: () => isDevAlreadyRunning
|
|
33709
33739
|
});
|
|
33710
33740
|
import { mkdirSync as mkdirSync5 } from "node:fs";
|
|
33711
|
-
import { dirname as dirname14, join as
|
|
33741
|
+
import { dirname as dirname14, join as join12, resolve as resolve10 } from "node:path";
|
|
33712
33742
|
async function isDevAlreadyRunning() {
|
|
33713
33743
|
if (await isDevRunning()) {
|
|
33714
33744
|
const running = await getRunningServices();
|
|
@@ -34317,7 +34347,7 @@ async function ensureDevPostgres(dataDir) {
|
|
|
34317
34347
|
if (check.stdout.toString().trim()) {
|
|
34318
34348
|
return false;
|
|
34319
34349
|
}
|
|
34320
|
-
const pgDataDir =
|
|
34350
|
+
const pgDataDir = join12(dataDir, "postgres");
|
|
34321
34351
|
mkdirSync5(pgDataDir, { recursive: true });
|
|
34322
34352
|
const stopped = await Bun.$`docker ps -aq -f name=secondlayer-dev-postgres`.quiet().nothrow();
|
|
34323
34353
|
if (stopped.stdout.toString().trim()) {
|
|
@@ -34749,75 +34779,12 @@ var init_node_impl = __esm(() => {
|
|
|
34749
34779
|
init_output();
|
|
34750
34780
|
});
|
|
34751
34781
|
|
|
34752
|
-
// src/
|
|
34753
|
-
|
|
34754
|
-
__export(exports_init, {
|
|
34755
|
-
init: () => init2
|
|
34756
|
-
});
|
|
34757
|
-
import { promises as fs7 } from "node:fs";
|
|
34758
|
-
import path11 from "node:path";
|
|
34759
|
-
async function init2() {
|
|
34760
|
-
const configPath = path11.join(process.cwd(), "secondlayer.config.ts");
|
|
34761
|
-
try {
|
|
34762
|
-
await fs7.access(configPath);
|
|
34763
|
-
console.log(source_default.yellow("⚠ secondlayer.config.ts already exists"));
|
|
34764
|
-
return;
|
|
34765
|
-
} catch {}
|
|
34766
|
-
const hasClarinetProject = await fileExists2("./Clarinet.toml");
|
|
34767
|
-
let config;
|
|
34768
|
-
if (hasClarinetProject) {
|
|
34769
|
-
config = `import { defineConfig } from '@secondlayer/cli';
|
|
34770
|
-
import { clarinet } from '@secondlayer/cli/plugins';
|
|
34771
|
-
|
|
34772
|
-
export default defineConfig({
|
|
34773
|
-
out: './src/generated/contracts.ts',
|
|
34774
|
-
plugins: [
|
|
34775
|
-
clarinet() // Found Clarinet.toml in current directory
|
|
34776
|
-
]
|
|
34777
|
-
});`;
|
|
34778
|
-
} else {
|
|
34779
|
-
config = `import { defineConfig } from '@secondlayer/cli';
|
|
34780
|
-
|
|
34781
|
-
export default defineConfig({
|
|
34782
|
-
out: './src/generated/contracts.ts',
|
|
34783
|
-
plugins: [],
|
|
34784
|
-
});`;
|
|
34785
|
-
}
|
|
34786
|
-
await fs7.writeFile(configPath, config);
|
|
34787
|
-
console.log(source_default.green("✓ Created `secondlayer.config.ts`"));
|
|
34788
|
-
console.log("\nRun `secondlayer generate` to generate type-safe interfaces, functions, and hooks!");
|
|
34789
|
-
}
|
|
34790
|
-
async function fileExists2(filePath) {
|
|
34791
|
-
try {
|
|
34792
|
-
await fs7.access(filePath);
|
|
34793
|
-
return true;
|
|
34794
|
-
} catch {
|
|
34795
|
-
return false;
|
|
34796
|
-
}
|
|
34797
|
-
}
|
|
34798
|
-
var init_init = __esm(() => {
|
|
34799
|
-
init_source();
|
|
34800
|
-
});
|
|
34801
|
-
|
|
34802
|
-
// ../../node_modules/commander/esm.mjs
|
|
34803
|
-
var import__ = __toESM(require_commander(), 1);
|
|
34804
|
-
var {
|
|
34805
|
-
program,
|
|
34806
|
-
createCommand,
|
|
34807
|
-
createArgument,
|
|
34808
|
-
createOption,
|
|
34809
|
-
CommanderError,
|
|
34810
|
-
InvalidArgumentError,
|
|
34811
|
-
InvalidOptionArgumentError,
|
|
34812
|
-
Command,
|
|
34813
|
-
Argument,
|
|
34814
|
-
Option,
|
|
34815
|
-
Help
|
|
34816
|
-
} = import__.default;
|
|
34782
|
+
// src/cli.ts
|
|
34783
|
+
init_esm();
|
|
34817
34784
|
// package.json
|
|
34818
34785
|
var package_default = {
|
|
34819
34786
|
name: "@secondlayer/cli",
|
|
34820
|
-
version: "
|
|
34787
|
+
version: "10.0.0",
|
|
34821
34788
|
repository: {
|
|
34822
34789
|
type: "git",
|
|
34823
34790
|
url: "git+https://github.com/ryanwaits/secondlayer.git",
|
|
@@ -34861,7 +34828,7 @@ var package_default = {
|
|
|
34861
34828
|
"@secondlayer/bundler": "^0.4.1",
|
|
34862
34829
|
"@secondlayer/scaffold": "^1.5.1",
|
|
34863
34830
|
"@secondlayer/sdk": "^6.51.0",
|
|
34864
|
-
"@secondlayer/shared": "^7.
|
|
34831
|
+
"@secondlayer/shared": "^7.9.0",
|
|
34865
34832
|
"@secondlayer/stacks": "^3.3.1",
|
|
34866
34833
|
"@secondlayer/subgraphs": "^3.26.4",
|
|
34867
34834
|
esbuild: "^0.27.4",
|
|
@@ -35003,7 +34970,7 @@ async function applyProfileUpdate(options, getOptionValueSource) {
|
|
|
35003
34970
|
});
|
|
35004
34971
|
}
|
|
35005
34972
|
function registerAccountCommand(program2) {
|
|
35006
|
-
const account = program2.command("account").description("Manage your account profile");
|
|
34973
|
+
const account = program2.command("account", { hidden: true }).description("Manage your account profile");
|
|
35007
34974
|
addBillingCommand(account);
|
|
35008
34975
|
account.command("get").description("Show your account profile").option("--json", "Output as JSON").action(withErrorHandling(async (options) => showProfile(options.json), { action: "show profile" }));
|
|
35009
34976
|
account.command("update").description("Update your public profile").option("--name <name>", "Set display name").option("--bio <bio>", "Set bio").option("--slug <slug>", "Set public URL slug").option("--notify-reindex-complete", "Email me when a subgraph reindex finishes").option("--no-notify-reindex-complete", "Don't email me when a subgraph reindex finishes").option("--json", "Output as JSON").action(withErrorHandling(async (options, command) => applyProfileUpdate(options, (key) => command.getOptionValueSource(key)), {
|
|
@@ -36522,7 +36489,7 @@ POST result (client-side, not logged):`));
|
|
|
36522
36489
|
// src/commands/devnet.ts
|
|
36523
36490
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
36524
36491
|
import { existsSync as existsSync6, mkdirSync as mkdirSync4, writeFileSync as writeFileSync6 } from "node:fs";
|
|
36525
|
-
import { dirname as dirname8, join as
|
|
36492
|
+
import { dirname as dirname8, join as join9 } from "node:path";
|
|
36526
36493
|
|
|
36527
36494
|
// src/lib/devnet-compose.ts
|
|
36528
36495
|
var DEFAULT_IMAGE_OWNER = "ryanwaits";
|
|
@@ -36536,6 +36503,7 @@ function buildDevnetCompose(opts = {}) {
|
|
|
36536
36503
|
const indexerImage = `ghcr.io/${owner}/secondlayer-indexer:${tag}`;
|
|
36537
36504
|
const dbUrl = "postgres://secondlayer:secondlayer@postgres:5432/secondlayer";
|
|
36538
36505
|
const DEV_SECRETS_KEY = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
|
|
36506
|
+
const DEV_INSTANCE_TOKEN = "dev-instance-token";
|
|
36539
36507
|
return `# Generated by \`sl devnet connect\` — do not edit by hand.
|
|
36540
36508
|
# Secondlayer app services for local clarinet devnet development.
|
|
36541
36509
|
name: secondlayer-devnet
|
|
@@ -36572,6 +36540,8 @@ services:
|
|
|
36572
36540
|
INSTANCE_MODE: oss
|
|
36573
36541
|
DATABASE_URL: ${dbUrl}
|
|
36574
36542
|
PORT: 3800
|
|
36543
|
+
LISTEN_HOST: "0.0.0.0"
|
|
36544
|
+
INSTANCE_TOKEN: ${DEV_INSTANCE_TOKEN}
|
|
36575
36545
|
NODE_ENV: production
|
|
36576
36546
|
DATA_DIR: /data
|
|
36577
36547
|
# Shared across api + subgraph-processor so the secret the api encrypts
|
|
@@ -36719,7 +36689,138 @@ stacks_node_events_observers = [${entry}]
|
|
|
36719
36689
|
|
|
36720
36690
|
// src/commands/devnet.ts
|
|
36721
36691
|
init_output();
|
|
36722
|
-
|
|
36692
|
+
|
|
36693
|
+
// src/commands/faucet.ts
|
|
36694
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
36695
|
+
import { readFile as readFile3 } from "node:fs/promises";
|
|
36696
|
+
import { join as join8 } from "node:path";
|
|
36697
|
+
import { http as http3, createWalletClient } from "@secondlayer/stacks";
|
|
36698
|
+
import { mnemonicToAccount } from "@secondlayer/stacks/accounts";
|
|
36699
|
+
import { devnet } from "@secondlayer/stacks/chains";
|
|
36700
|
+
import { Cl } from "@secondlayer/stacks/clarity";
|
|
36701
|
+
import { parse as parseToml2 } from "smol-toml";
|
|
36702
|
+
init_output();
|
|
36703
|
+
var DUST_LIMIT_SATS = 546n;
|
|
36704
|
+
var DEFAULT_AMOUNT_SATS = 1000000n;
|
|
36705
|
+
async function readDeployerMnemonic(projectDir) {
|
|
36706
|
+
const path11 = join8(projectDir, "settings", "Devnet.toml");
|
|
36707
|
+
let raw;
|
|
36708
|
+
try {
|
|
36709
|
+
raw = await readFile3(path11, "utf8");
|
|
36710
|
+
} catch {
|
|
36711
|
+
throw new Error(`could not read ${path11}`);
|
|
36712
|
+
}
|
|
36713
|
+
const parsed = parseToml2(raw);
|
|
36714
|
+
const mnemonic = parsed.accounts?.deployer?.mnemonic;
|
|
36715
|
+
if (!mnemonic) {
|
|
36716
|
+
throw new Error(`no accounts.deployer.mnemonic in ${path11}`);
|
|
36717
|
+
}
|
|
36718
|
+
return mnemonic;
|
|
36719
|
+
}
|
|
36720
|
+
async function readBurnAnchor(rpcUrl) {
|
|
36721
|
+
const info2 = await (await fetch(`${rpcUrl.replace(/\/$/, "")}/v2/info`, {
|
|
36722
|
+
signal: AbortSignal.timeout(1e4)
|
|
36723
|
+
})).json();
|
|
36724
|
+
const height = info2.burn_block_height;
|
|
36725
|
+
if (typeof height !== "number") {
|
|
36726
|
+
throw new Error("node did not report a burn block height");
|
|
36727
|
+
}
|
|
36728
|
+
const burn = await (await fetch(`${rpcUrl.replace(/\/$/, "")}/extended/v2/burn-blocks/${height}`, { signal: AbortSignal.timeout(1e4) })).json();
|
|
36729
|
+
if (!burn.burn_block_hash) {
|
|
36730
|
+
throw new Error(`node did not report a hash for burn block ${height}`);
|
|
36731
|
+
}
|
|
36732
|
+
return { height, hash: burn.burn_block_hash.replace(/^0x/, "") };
|
|
36733
|
+
}
|
|
36734
|
+
function parseAmount(value) {
|
|
36735
|
+
if (value === undefined)
|
|
36736
|
+
return DEFAULT_AMOUNT_SATS;
|
|
36737
|
+
const parsed = BigInt(value);
|
|
36738
|
+
if (parsed < DUST_LIMIT_SATS) {
|
|
36739
|
+
throw new Error(`--amount must be at least ${DUST_LIMIT_SATS} sats (the protocol dust limit), got ${parsed}`);
|
|
36740
|
+
}
|
|
36741
|
+
return parsed;
|
|
36742
|
+
}
|
|
36743
|
+
function registerFaucetCommand(devnetCommand) {
|
|
36744
|
+
devnetCommand.command("faucet").description("Mint mock sBTC to an address on your devnet").requiredOption("--to <address>", "recipient Stacks address").option("--amount <sats>", `amount in sats (default ${DEFAULT_AMOUNT_SATS}, minimum ${DUST_LIMIT_SATS})`).option("--contract <id>", "sBTC deposit contract (default: <deployer>.sbtc-deposit)").option("--rpc-url <url>", "devnet node RPC (default http://localhost:3999)").option("--json", "Output as JSON").addHelpText("after", `
|
|
36745
|
+
Examples:
|
|
36746
|
+
$ sl devnet faucet --to ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM
|
|
36747
|
+
$ sl devnet faucet --to ST2... --amount 5000000
|
|
36748
|
+
|
|
36749
|
+
Requires a devnet running with the sBTC contract set deployed by your
|
|
36750
|
+
Clarinet deployer — that deployer is the signer set on your own chain, which
|
|
36751
|
+
is what makes minting possible without Bitcoin.`).action(async (opts) => {
|
|
36752
|
+
try {
|
|
36753
|
+
const network = process.env.STACKS_NETWORK ?? "devnet";
|
|
36754
|
+
if (network !== "devnet" && network !== "local") {
|
|
36755
|
+
printError(`sl devnet faucet only works on devnet, not ${network}.`, {
|
|
36756
|
+
hint: "Mock sBTC is mintable because you deployed the contracts. On testnet or mainnet the signer set is not yours."
|
|
36757
|
+
});
|
|
36758
|
+
process.exit(2);
|
|
36759
|
+
}
|
|
36760
|
+
const rpcUrl = opts.rpcUrl ?? "http://localhost:3999";
|
|
36761
|
+
const amount = parseAmount(opts.amount);
|
|
36762
|
+
const projectDir = findClarinetProject(process.cwd());
|
|
36763
|
+
if (!projectDir) {
|
|
36764
|
+
printError("No Clarinet project found.", {
|
|
36765
|
+
hint: "Run this from a project containing Clarinet.toml, or pass --contract explicitly."
|
|
36766
|
+
});
|
|
36767
|
+
process.exit(2);
|
|
36768
|
+
}
|
|
36769
|
+
const mnemonic = await readDeployerMnemonic(projectDir);
|
|
36770
|
+
const account = mnemonicToAccount(mnemonic, {
|
|
36771
|
+
addressVersion: devnet.addressVersion.singleSig
|
|
36772
|
+
});
|
|
36773
|
+
const contract = opts.contract ?? `${account.address}.sbtc-deposit`;
|
|
36774
|
+
const anchor = await readBurnAnchor(rpcUrl);
|
|
36775
|
+
const client2 = createWalletClient({
|
|
36776
|
+
account,
|
|
36777
|
+
chain: devnet,
|
|
36778
|
+
transport: http3(rpcUrl)
|
|
36779
|
+
});
|
|
36780
|
+
const depositTxid = randomBytes2(32).toString("hex");
|
|
36781
|
+
const sweepTxid = randomBytes2(32).toString("hex");
|
|
36782
|
+
note(` minting ${amount} sats to ${opts.to}`);
|
|
36783
|
+
note(` via ${contract} as signer ${account.address}`);
|
|
36784
|
+
const txid = await client2.callContract({
|
|
36785
|
+
contract,
|
|
36786
|
+
functionName: "complete-deposit-wrapper",
|
|
36787
|
+
functionArgs: [
|
|
36788
|
+
Cl.bufferFromHex(depositTxid),
|
|
36789
|
+
Cl.uint(0),
|
|
36790
|
+
Cl.uint(amount),
|
|
36791
|
+
Cl.principal(opts.to),
|
|
36792
|
+
Cl.bufferFromHex(anchor.hash),
|
|
36793
|
+
Cl.uint(anchor.height),
|
|
36794
|
+
Cl.bufferFromHex(sweepTxid)
|
|
36795
|
+
],
|
|
36796
|
+
postConditionMode: "allow"
|
|
36797
|
+
});
|
|
36798
|
+
output({
|
|
36799
|
+
json: opts.json,
|
|
36800
|
+
data: {
|
|
36801
|
+
txid,
|
|
36802
|
+
recipient: opts.to,
|
|
36803
|
+
amount_sats: amount.toString(),
|
|
36804
|
+
contract,
|
|
36805
|
+
burn_height: anchor.height
|
|
36806
|
+
},
|
|
36807
|
+
human: () => {
|
|
36808
|
+
success(`Minted ${amount} sats of mock sBTC to ${opts.to}.`);
|
|
36809
|
+
note(` tx ${txid}`);
|
|
36810
|
+
console.error(dim(" Mock sBTC on your devnet — not the canonical asset."));
|
|
36811
|
+
}
|
|
36812
|
+
});
|
|
36813
|
+
} catch (err) {
|
|
36814
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
36815
|
+
const hint = /ECONNREFUSED|fetch failed/.test(message) ? "Is your devnet running? Start it with `clarinet devnet start`." : /NoSuchContract|does not exist/.test(message) ? "Deploy the sBTC contract set to your devnet first, or pass --contract." : "Check that your Clarinet deployer matches the sBTC contract deployer.";
|
|
36816
|
+
printError(message, { hint });
|
|
36817
|
+
process.exit(1);
|
|
36818
|
+
}
|
|
36819
|
+
});
|
|
36820
|
+
}
|
|
36821
|
+
|
|
36822
|
+
// src/commands/devnet.ts
|
|
36823
|
+
var COMPOSE_REL = join9(".secondlayer", "docker-compose.yml");
|
|
36723
36824
|
function ensureDocker() {
|
|
36724
36825
|
const probe = spawnSync2("docker", ["info"], { stdio: "ignore" });
|
|
36725
36826
|
if (probe.error || probe.status !== 0) {
|
|
@@ -36747,23 +36848,24 @@ var SERVICES = [
|
|
|
36747
36848
|
"migrate"
|
|
36748
36849
|
];
|
|
36749
36850
|
function registerDevnetCommand(program2) {
|
|
36750
|
-
const
|
|
36751
|
-
|
|
36851
|
+
const devnet2 = program2.command("devnet").description("Run Secondlayer services against a local Clarinet devnet");
|
|
36852
|
+
registerFaucetCommand(devnet2);
|
|
36853
|
+
devnet2.command("connect").description("Point your clarinet project's devnet at a local Secondlayer stack and start it").option("--project <dir>", "Clarinet project directory (defaults to the nearest Clarinet.toml)").option("--image-tag <tag>", "Published image tag to run", DEFAULT_IMAGE_TAG).option("--owner <owner>", "ghcr image owner (namespace) to pull from").option("--no-up", "Patch config + write compose without starting docker").action(async (options3) => {
|
|
36752
36854
|
await connect(options3);
|
|
36753
36855
|
});
|
|
36754
|
-
|
|
36856
|
+
devnet2.command("down").description("Stop the local Secondlayer stack started by `sl devnet connect`").option("--project <dir>", "Clarinet project directory").option("--purge", "Also remove volumes (wipes the local index)").action(async (options3) => {
|
|
36755
36857
|
await down(options3);
|
|
36756
36858
|
});
|
|
36757
|
-
|
|
36859
|
+
devnet2.command("status").description("Snapshot of the local stack: ingest, subgraphs, and recent activity").option("-w, --watch", "Refresh every 2s until Ctrl-C").option("--limit <n>", "Recent activity rows to show", "12").action(async (options3) => {
|
|
36758
36860
|
await status(options3);
|
|
36759
36861
|
});
|
|
36760
|
-
|
|
36862
|
+
devnet2.command("logs [service]").description("Tail stack logs — all services, or one of: indexer, api, subgraph-processor, postgres").option("--project <dir>", "Clarinet project directory").option("-f, --follow", "Follow log output").option("-n, --lines <n>", "Lines to show from the end of each log").action(async (service, options3) => {
|
|
36761
36863
|
await logs(service, options3);
|
|
36762
36864
|
});
|
|
36763
36865
|
}
|
|
36764
36866
|
function resolveProject(explicit) {
|
|
36765
36867
|
if (explicit) {
|
|
36766
|
-
if (!existsSync6(
|
|
36868
|
+
if (!existsSync6(join9(explicit, "Clarinet.toml"))) {
|
|
36767
36869
|
error(`No Clarinet.toml in ${explicit}`);
|
|
36768
36870
|
process.exit(1);
|
|
36769
36871
|
}
|
|
@@ -36779,7 +36881,7 @@ function resolveProject(explicit) {
|
|
|
36779
36881
|
}
|
|
36780
36882
|
async function connect(options3) {
|
|
36781
36883
|
const project = resolveProject(options3.project);
|
|
36782
|
-
const devnetToml =
|
|
36884
|
+
const devnetToml = join9(project, "settings", "Devnet.toml");
|
|
36783
36885
|
mkdirSync4(dirname8(devnetToml), { recursive: true });
|
|
36784
36886
|
const result = ensureEventObserver(devnetToml, DEFAULT_INDEXER_OBSERVER);
|
|
36785
36887
|
console.log(`${green("✓")} found Clarinet.toml`);
|
|
@@ -36789,7 +36891,7 @@ async function connect(options3) {
|
|
|
36789
36891
|
console.log(`${green("✓")} ${result.status === "created" ? "created" : "patched"} settings/Devnet.toml`);
|
|
36790
36892
|
console.log(dim(` + stacks_node_events_observers = ["${DEFAULT_INDEXER_OBSERVER}"]`));
|
|
36791
36893
|
}
|
|
36792
|
-
const composePath =
|
|
36894
|
+
const composePath = join9(project, COMPOSE_REL);
|
|
36793
36895
|
mkdirSync4(dirname8(composePath), { recursive: true });
|
|
36794
36896
|
writeFileSync6(composePath, buildDevnetCompose({ owner: options3.owner, imageTag: options3.imageTag }));
|
|
36795
36897
|
console.log(`${green("✓")} wrote ${COMPOSE_REL}`);
|
|
@@ -36819,7 +36921,7 @@ Stop with: sl devnet down`));
|
|
|
36819
36921
|
}
|
|
36820
36922
|
async function down(options3) {
|
|
36821
36923
|
const project = resolveProject(options3.project);
|
|
36822
|
-
const composePath =
|
|
36924
|
+
const composePath = join9(project, COMPOSE_REL);
|
|
36823
36925
|
if (!existsSync6(composePath)) {
|
|
36824
36926
|
error(`No ${COMPOSE_REL} in ${project} — nothing to stop. Run \`sl devnet connect\` first.`);
|
|
36825
36927
|
process.exit(1);
|
|
@@ -36834,7 +36936,7 @@ async function down(options3) {
|
|
|
36834
36936
|
}
|
|
36835
36937
|
async function logs(service, options3) {
|
|
36836
36938
|
const project = resolveProject(options3.project);
|
|
36837
|
-
const composePath =
|
|
36939
|
+
const composePath = join9(project, COMPOSE_REL);
|
|
36838
36940
|
if (!existsSync6(composePath)) {
|
|
36839
36941
|
error(`No ${COMPOSE_REL} in ${project} — run \`sl devnet connect\` first.`);
|
|
36840
36942
|
process.exit(1);
|
|
@@ -36960,7 +37062,7 @@ init_subgraphs2();
|
|
|
36960
37062
|
init_output();
|
|
36961
37063
|
init_resolve_auth();
|
|
36962
37064
|
import { mkdir as mkdir3, writeFile as writeFile3 } from "node:fs/promises";
|
|
36963
|
-
import { dirname as dirname9, join as
|
|
37065
|
+
import { dirname as dirname9, join as join10 } from "node:path";
|
|
36964
37066
|
import { createStreamsClient } from "@secondlayer/sdk";
|
|
36965
37067
|
import { STREAMS_EVENT_TYPES } from "@secondlayer/shared";
|
|
36966
37068
|
var DEFAULT_BASE_URL3 = "https://api.secondlayer.tools";
|
|
@@ -37136,7 +37238,7 @@ Examples:
|
|
|
37136
37238
|
note(`# downloading ${files.length} file(s) to ${options3.to}`);
|
|
37137
37239
|
for (const file of files) {
|
|
37138
37240
|
const bytes = await dumps.download(file);
|
|
37139
|
-
const dest =
|
|
37241
|
+
const dest = join10(options3.to, file.path);
|
|
37140
37242
|
await mkdir3(dirname9(dest), { recursive: true });
|
|
37141
37243
|
await writeFile3(dest, bytes);
|
|
37142
37244
|
process.stderr.write(`# ${file.path} (${file.row_count} rows, ${bytes.byteLength} bytes)
|
|
@@ -37185,7 +37287,7 @@ init_resolve_auth();
|
|
|
37185
37287
|
import { SecondLayer as SecondLayer4 } from "@secondlayer/sdk";
|
|
37186
37288
|
var DEFAULT_BASE_URL4 = "https://api.secondlayer.tools";
|
|
37187
37289
|
function registerKeysCommand(program2) {
|
|
37188
|
-
const keys = program2.command("keys").description("Manage API keys");
|
|
37290
|
+
const keys = program2.command("keys", { hidden: true }).description("Manage API keys");
|
|
37189
37291
|
keys.command("create").description("Mint a scoped streams/index read key (requires an account-level key)").option("--product <product>", "Key scope: streams or index", "streams").option("--name <name>", "Optional label for the key").option("--json", "Print the full response as JSON").action(async (o3) => {
|
|
37190
37292
|
if (o3.product && o3.product !== "streams" && o3.product !== "index") {
|
|
37191
37293
|
error("--product must be 'streams' or 'index'");
|
|
@@ -38424,9 +38526,9 @@ init_output();
|
|
|
38424
38526
|
|
|
38425
38527
|
// src/lib/project-file.ts
|
|
38426
38528
|
import { existsSync as existsSync7 } from "node:fs";
|
|
38427
|
-
import { mkdir as mkdir4, readFile as
|
|
38529
|
+
import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4 } from "node:fs/promises";
|
|
38428
38530
|
import { homedir as homedir5 } from "node:os";
|
|
38429
|
-
import { dirname as dirname15, join as
|
|
38531
|
+
import { dirname as dirname15, join as join13, parse as parsePath2, resolve as resolve11 } from "node:path";
|
|
38430
38532
|
import { z as z4 } from "zod";
|
|
38431
38533
|
var ProjectFileSchema = z4.object({
|
|
38432
38534
|
slug: z4.string().min(1)
|
|
@@ -38438,15 +38540,15 @@ async function readActiveProject(cwd, globalDefault) {
|
|
|
38438
38540
|
const home2 = resolve11(homedir5());
|
|
38439
38541
|
const fsRoot = parsePath2(dir).root;
|
|
38440
38542
|
while (true) {
|
|
38441
|
-
const candidate =
|
|
38543
|
+
const candidate = join13(dir, DIRNAME, FILENAME);
|
|
38442
38544
|
if (existsSync7(candidate)) {
|
|
38443
38545
|
try {
|
|
38444
|
-
const raw = await
|
|
38546
|
+
const raw = await readFile4(candidate, "utf8");
|
|
38445
38547
|
const parsed = ProjectFileSchema.parse(JSON.parse(raw));
|
|
38446
38548
|
return { slug: parsed.slug, resolvedFrom: candidate };
|
|
38447
38549
|
} catch {}
|
|
38448
38550
|
}
|
|
38449
|
-
if (existsSync7(
|
|
38551
|
+
if (existsSync7(join13(dir, ".git")))
|
|
38450
38552
|
break;
|
|
38451
38553
|
if (dir === home2 || dir === fsRoot)
|
|
38452
38554
|
break;
|
|
@@ -38458,15 +38560,15 @@ async function readActiveProject(cwd, globalDefault) {
|
|
|
38458
38560
|
if (globalDefault) {
|
|
38459
38561
|
return {
|
|
38460
38562
|
slug: globalDefault,
|
|
38461
|
-
resolvedFrom:
|
|
38563
|
+
resolvedFrom: join13(homedir5(), ".secondlayer", "config.json")
|
|
38462
38564
|
};
|
|
38463
38565
|
}
|
|
38464
38566
|
return null;
|
|
38465
38567
|
}
|
|
38466
38568
|
async function writeActiveProject(slug, cwd) {
|
|
38467
|
-
const dir =
|
|
38569
|
+
const dir = join13(resolve11(cwd), DIRNAME);
|
|
38468
38570
|
await mkdir4(dir, { recursive: true });
|
|
38469
|
-
const file =
|
|
38571
|
+
const file = join13(dir, FILENAME);
|
|
38470
38572
|
await writeFile4(file, `${JSON.stringify({ slug }, null, 2)}
|
|
38471
38573
|
`, "utf8");
|
|
38472
38574
|
return file;
|
|
@@ -38475,7 +38577,7 @@ async function writeActiveProject(slug, cwd) {
|
|
|
38475
38577
|
// src/commands/whoami.ts
|
|
38476
38578
|
init_resolve_auth();
|
|
38477
38579
|
function registerWhoamiCommand(program2) {
|
|
38478
|
-
program2.command("whoami").description("Show the active account, credential source, and project").option("--json", "Output as JSON").action(async (options3) => {
|
|
38580
|
+
program2.command("whoami", { hidden: true }).description("Show the active account, credential source, and project").option("--json", "Output as JSON").action(async (options3) => {
|
|
38479
38581
|
let auth;
|
|
38480
38582
|
try {
|
|
38481
38583
|
auth = await resolveAuth();
|
|
@@ -38531,7 +38633,7 @@ init_http();
|
|
|
38531
38633
|
init_output();
|
|
38532
38634
|
init_session();
|
|
38533
38635
|
function registerLogoutCommand(program2) {
|
|
38534
|
-
program2.command("logout").description("Log out and revoke the local session").action(async () => {
|
|
38636
|
+
program2.command("logout", { hidden: true }).description("Log out and revoke the local session").action(async () => {
|
|
38535
38637
|
const session = await readSession();
|
|
38536
38638
|
if (!session) {
|
|
38537
38639
|
info("Not logged in.");
|
|
@@ -38553,7 +38655,7 @@ init_config2();
|
|
|
38553
38655
|
init_http();
|
|
38554
38656
|
init_output();
|
|
38555
38657
|
function registerProjectCommand(program2) {
|
|
38556
|
-
const project = program2.command("projects").alias("project").description("Manage your Secondlayer project");
|
|
38658
|
+
const project = program2.command("projects", { hidden: true }).alias("project").description("Manage your Secondlayer project");
|
|
38557
38659
|
project.command("list").alias("ls").description("List projects in your account").option("--json", "Output as JSON").action(async (options3) => {
|
|
38558
38660
|
try {
|
|
38559
38661
|
const res = await httpPlatform("/api/projects");
|
|
@@ -38632,12 +38734,16 @@ import {
|
|
|
38632
38734
|
compareRangeDigests,
|
|
38633
38735
|
computeRangeDigest
|
|
38634
38736
|
} from "@secondlayer/shared/archive/range-digest";
|
|
38737
|
+
import {
|
|
38738
|
+
comparePartitionSemanticDigests,
|
|
38739
|
+
computePartitionSemanticDigest
|
|
38740
|
+
} from "@secondlayer/shared/archive/semantic-digest-builder";
|
|
38635
38741
|
import { getDb as getDb2 } from "@secondlayer/shared/db";
|
|
38636
38742
|
|
|
38637
38743
|
// src/lib/archive-reference.ts
|
|
38638
38744
|
import { createHash as createHash2 } from "node:crypto";
|
|
38639
|
-
import { readFile as
|
|
38640
|
-
import { dirname as dirname16, join as
|
|
38745
|
+
import { readFile as readFile5 } from "node:fs/promises";
|
|
38746
|
+
import { dirname as dirname16, join as join14, resolve as resolve12 } from "node:path";
|
|
38641
38747
|
import {
|
|
38642
38748
|
checkKeyTrust,
|
|
38643
38749
|
publicKeyFor,
|
|
@@ -38666,7 +38772,7 @@ async function loadReference(source) {
|
|
|
38666
38772
|
root = url.toString();
|
|
38667
38773
|
} else {
|
|
38668
38774
|
const path11 = resolve12(source);
|
|
38669
|
-
manifest = JSON.parse(await
|
|
38775
|
+
manifest = JSON.parse(await readFile5(path11, "utf8"));
|
|
38670
38776
|
root = isLatestPointer(manifest) ? dirname16(path11) : dirname16(dirname16(path11));
|
|
38671
38777
|
}
|
|
38672
38778
|
if (!isLatestPointer(manifest)) {
|
|
@@ -38674,7 +38780,7 @@ async function loadReference(source) {
|
|
|
38674
38780
|
}
|
|
38675
38781
|
const pointer = manifest;
|
|
38676
38782
|
const snapshotPath = pointer.snapshot_path;
|
|
38677
|
-
const resolved = isRemote ? await fetchJson(`${root.replace(/\/$/, "")}/${snapshotPath}`) : JSON.parse(await
|
|
38783
|
+
const resolved = isRemote ? await fetchJson(`${root.replace(/\/$/, "")}/${snapshotPath}`) : JSON.parse(await readFile5(join14(root, snapshotPath), "utf8"));
|
|
38678
38784
|
if (pointer.snapshot_digest) {
|
|
38679
38785
|
const digest = createHash2("sha256").update(JSON.stringify((({ signature: _s, key_id: _k, ...rest }) => rest)(resolved))).digest("hex");
|
|
38680
38786
|
if (digest !== pointer.snapshot_digest) {
|
|
@@ -38708,6 +38814,7 @@ function checkSignature(manifest, publicKeyPem, insecure) {
|
|
|
38708
38814
|
};
|
|
38709
38815
|
}
|
|
38710
38816
|
}
|
|
38817
|
+
var HOSTED_API = "https://api.secondlayer.tools";
|
|
38711
38818
|
async function resolvePublicKey(explicitPem, apiUrl) {
|
|
38712
38819
|
if (explicitPem)
|
|
38713
38820
|
return explicitPem;
|
|
@@ -38721,6 +38828,16 @@ async function resolvePublicKey(explicitPem, apiUrl) {
|
|
|
38721
38828
|
return;
|
|
38722
38829
|
}
|
|
38723
38830
|
}
|
|
38831
|
+
async function resolveArchivePublicKey(input4) {
|
|
38832
|
+
if (input4.explicitPem)
|
|
38833
|
+
return input4.explicitPem;
|
|
38834
|
+
if (input4.envPem)
|
|
38835
|
+
return input4.envPem;
|
|
38836
|
+
if (input4.allowHostedApi) {
|
|
38837
|
+
return resolvePublicKey(undefined, HOSTED_API);
|
|
38838
|
+
}
|
|
38839
|
+
return;
|
|
38840
|
+
}
|
|
38724
38841
|
async function fetchVerifiedPartition(reference, partition) {
|
|
38725
38842
|
let bytes;
|
|
38726
38843
|
if (reference.isRemote) {
|
|
@@ -38731,7 +38848,7 @@ async function fetchVerifiedPartition(reference, partition) {
|
|
|
38731
38848
|
}
|
|
38732
38849
|
bytes = Buffer.from(await response2.arrayBuffer());
|
|
38733
38850
|
} else {
|
|
38734
|
-
bytes = await
|
|
38851
|
+
bytes = await readFile5(join14(reference.root, partition.path));
|
|
38735
38852
|
}
|
|
38736
38853
|
const digest = createHash2("sha256").update(bytes).digest("hex");
|
|
38737
38854
|
if (digest !== partition.sha256) {
|
|
@@ -38764,7 +38881,7 @@ function statusLabel(status2) {
|
|
|
38764
38881
|
return yellow("missing");
|
|
38765
38882
|
}
|
|
38766
38883
|
function registerVerifyCommand(program2) {
|
|
38767
|
-
program2.command("verify").description("Compare local chain data against a signed archive (read-only; nothing is uploaded)").requiredOption("--against <manifest>", "archive manifest: an https URL or a local file path").option("--from-block <n>", "first height to check").option("--to-block <n>", "last height to check").option("--counts", "also compare transaction/event row counts (slower)").option("--public-key <pem>", "pin the signing key instead of fetching it").option("--insecure", "skip the manifest signature check (not recommended)").option("--json", "Output as JSON").addHelpText("after", `
|
|
38884
|
+
program2.command("verify").description("Compare local chain data against a signed archive (read-only; nothing is uploaded)").requiredOption("--against <manifest>", "archive manifest: an https URL or a local file path").option("--from-block <n>", "first height to check").option("--to-block <n>", "last height to check").option("--counts", "also compare transaction/event row counts (slower)").option("--semantic", "also recompute per-partition semantic digests locally and compare (slow: full re-stream)").option("--public-key <pem>", "pin the signing key instead of fetching it").option("--insecure", "skip the manifest signature check (not recommended)").option("--json", "Output as JSON").addHelpText("after", `
|
|
38768
38885
|
Examples:
|
|
38769
38886
|
$ sl verify --against https://archive.secondlayer.tools/.../snapshots/<digest>.json
|
|
38770
38887
|
$ sl verify --against ./snapshot.json --from-block 8000000 --to-block 8499999
|
|
@@ -38844,13 +38961,52 @@ Exit codes:
|
|
|
38844
38961
|
});
|
|
38845
38962
|
}
|
|
38846
38963
|
}
|
|
38847
|
-
const
|
|
38964
|
+
const semanticChecks = [];
|
|
38965
|
+
const referenceSemantic = (manifest.partition_semantic_digests ?? []).filter((d2) => {
|
|
38966
|
+
if (fromBlock !== undefined && d2.to_block < fromBlock)
|
|
38967
|
+
return false;
|
|
38968
|
+
if (toBlock !== undefined && d2.from_block > toBlock)
|
|
38969
|
+
return false;
|
|
38970
|
+
return true;
|
|
38971
|
+
});
|
|
38972
|
+
if (opts.semantic) {
|
|
38973
|
+
if (referenceSemantic.length === 0) {
|
|
38974
|
+
warn("Reference publishes no semantic digests; --semantic had nothing to compare.");
|
|
38975
|
+
} else {
|
|
38976
|
+
note(`Recomputing ${referenceSemantic.length} partition semantic digests (this is the slow pass).`);
|
|
38977
|
+
const localSemantic = [];
|
|
38978
|
+
let semanticDone = 0;
|
|
38979
|
+
for (const partition of referenceSemantic) {
|
|
38980
|
+
localSemantic.push(await computePartitionSemanticDigest(db, partition.dataset, partition.from_block, partition.to_block));
|
|
38981
|
+
semanticDone++;
|
|
38982
|
+
if (semanticDone % 5 === 0 || semanticDone === referenceSemantic.length) {
|
|
38983
|
+
note(` checked ${semanticDone}/${referenceSemantic.length} partitions`);
|
|
38984
|
+
}
|
|
38985
|
+
}
|
|
38986
|
+
semanticChecks.push(...comparePartitionSemanticDigests(localSemantic, referenceSemantic));
|
|
38987
|
+
}
|
|
38988
|
+
}
|
|
38989
|
+
const all2 = [
|
|
38990
|
+
...comparisons,
|
|
38991
|
+
...countChecks,
|
|
38992
|
+
...semanticChecks.map((s2) => ({
|
|
38993
|
+
dataset: s2.dataset,
|
|
38994
|
+
from_block: s2.from_block,
|
|
38995
|
+
to_block: s2.to_block,
|
|
38996
|
+
status: s2.status,
|
|
38997
|
+
expected_digest: s2.expected_digest,
|
|
38998
|
+
actual_digest: s2.actual_digest,
|
|
38999
|
+
expected_rows: s2.expected_rows,
|
|
39000
|
+
actual_rows: s2.actual_rows
|
|
39001
|
+
}))
|
|
39002
|
+
];
|
|
38848
39003
|
const diverged = all2.filter((c4) => c4.status !== "match");
|
|
38849
39004
|
const report = {
|
|
38850
39005
|
status: diverged.length === 0 ? "clean" : "diverged",
|
|
38851
39006
|
reference: origin,
|
|
38852
39007
|
signature_verified: signature.verified,
|
|
38853
39008
|
ranges_checked: all2.length,
|
|
39009
|
+
semantic_checks: semanticChecks.length,
|
|
38854
39010
|
divergent_ranges: diverged
|
|
38855
39011
|
};
|
|
38856
39012
|
output({
|
|
@@ -38891,7 +39047,7 @@ Exit codes:
|
|
|
38891
39047
|
// src/commands/repair.ts
|
|
38892
39048
|
import { unlink as unlink2, writeFile as writeFile5 } from "node:fs/promises";
|
|
38893
39049
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
38894
|
-
import { join as
|
|
39050
|
+
import { join as join15 } from "node:path";
|
|
38895
39051
|
import { ParquetReader } from "@dsnp/parquetjs";
|
|
38896
39052
|
import {
|
|
38897
39053
|
compareRangeDigests as compareRangeDigests2,
|
|
@@ -38914,7 +39070,7 @@ function asOptionalText(value) {
|
|
|
38914
39070
|
return value === null || value === undefined ? null : asText(value);
|
|
38915
39071
|
}
|
|
38916
39072
|
async function readBlocksPartition(bytes, label) {
|
|
38917
|
-
const path11 =
|
|
39073
|
+
const path11 = join15(tmpdir2(), `sl-repair-${label}-${process.pid}.parquet`);
|
|
38918
39074
|
await writeFile5(path11, bytes);
|
|
38919
39075
|
try {
|
|
38920
39076
|
const reader = await ParquetReader.openFile(path11);
|
|
@@ -39140,7 +39296,7 @@ Exit codes:
|
|
|
39140
39296
|
// src/commands/bootstrap.ts
|
|
39141
39297
|
import { unlink as unlink3, writeFile as writeFile6 } from "node:fs/promises";
|
|
39142
39298
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
39143
|
-
import { join as
|
|
39299
|
+
import { join as join16 } from "node:path";
|
|
39144
39300
|
import { ParquetReader as ParquetReader2 } from "@dsnp/parquetjs";
|
|
39145
39301
|
import { confirm as confirm7 } from "@inquirer/prompts";
|
|
39146
39302
|
import {
|
|
@@ -39149,7 +39305,35 @@ import {
|
|
|
39149
39305
|
} from "@secondlayer/shared/archive/copy-loader";
|
|
39150
39306
|
import { computeRangeDigest as computeRangeDigest3 } from "@secondlayer/shared/archive/range-digest";
|
|
39151
39307
|
import { getDb as getDb4, getRawClient, sql as sql3 } from "@secondlayer/shared/db";
|
|
39308
|
+
|
|
39309
|
+
// src/lib/bootstrap-resume.ts
|
|
39310
|
+
function planTornImport(input4) {
|
|
39311
|
+
if (input4.hasIndexProgress) {
|
|
39312
|
+
return {
|
|
39313
|
+
action: "refuse",
|
|
39314
|
+
reason: "This database already holds a completed bootstrap."
|
|
39315
|
+
};
|
|
39316
|
+
}
|
|
39317
|
+
if (input4.maxBlockHeight === null || input4.maxBlockHeight <= 0) {
|
|
39318
|
+
return { action: "fresh" };
|
|
39319
|
+
}
|
|
39320
|
+
const blockPartitions = input4.partitions.filter((p2) => p2.dataset === "blocks").sort((a4, b3) => a4.from_block - b3.from_block);
|
|
39321
|
+
const sealed = blockPartitions.filter((p2) => p2.to_block <= (input4.maxBlockHeight ?? 0));
|
|
39322
|
+
const skipThrough = sealed.length > 0 ? Math.max(...sealed.map((p2) => p2.to_block)) : 0;
|
|
39323
|
+
const torn = blockPartitions.find((p2) => p2.from_block <= (input4.maxBlockHeight ?? 0) && p2.to_block > (input4.maxBlockHeight ?? 0));
|
|
39324
|
+
return {
|
|
39325
|
+
action: "resume",
|
|
39326
|
+
truncateFrom: torn ? torn.from_block : null,
|
|
39327
|
+
skipThrough
|
|
39328
|
+
};
|
|
39329
|
+
}
|
|
39330
|
+
function partitionIsLoaded(partition, skipThrough) {
|
|
39331
|
+
return partition.to_block <= skipThrough;
|
|
39332
|
+
}
|
|
39333
|
+
|
|
39334
|
+
// src/commands/bootstrap.ts
|
|
39152
39335
|
init_output();
|
|
39336
|
+
init_resolve_auth();
|
|
39153
39337
|
var BOOTSTRAP_EXIT = {
|
|
39154
39338
|
OK: 0,
|
|
39155
39339
|
INCOMPLETE: 1,
|
|
@@ -39181,7 +39365,7 @@ function formatDuration2(seconds2) {
|
|
|
39181
39365
|
return `${(seconds2 / 86400).toFixed(1)} days`;
|
|
39182
39366
|
}
|
|
39183
39367
|
async function* readPartitionRows(bytes, label) {
|
|
39184
|
-
const path11 =
|
|
39368
|
+
const path11 = join16(tmpdir3(), `sl-bootstrap-${label}-${process.pid}.parquet`);
|
|
39185
39369
|
await writeFile6(path11, bytes);
|
|
39186
39370
|
try {
|
|
39187
39371
|
const reader = await ParquetReader2.openFile(path11);
|
|
@@ -39210,8 +39394,8 @@ async function loadPartition(reference, partition, rawClient) {
|
|
|
39210
39394
|
}
|
|
39211
39395
|
return written;
|
|
39212
39396
|
}
|
|
39213
|
-
function
|
|
39214
|
-
|
|
39397
|
+
function attachBootstrapCommand(cmd) {
|
|
39398
|
+
return cmd.requiredOption("--against <manifest>", "archive manifest: an https URL or a local file path").option("--to-block <n>", "stop at this height instead of the archive tip").option("--public-key <pem>", "pin the signing key instead of fetching it").option("-y, --yes", "skip the confirmation prompt").option("--json", "Output as JSON").addHelpText("after", `
|
|
39215
39399
|
Examples:
|
|
39216
39400
|
$ sl bootstrap --against https://archive.secondlayer.tools/.../snapshots/<digest>.json
|
|
39217
39401
|
$ sl bootstrap --against ./snapshot.json --to-block 4000000 --yes
|
|
@@ -39222,7 +39406,11 @@ Exit codes:
|
|
|
39222
39406
|
2 refused (non-empty target, or an untrusted reference)`).action(async (opts) => {
|
|
39223
39407
|
try {
|
|
39224
39408
|
const reference = await loadReference(opts.against);
|
|
39225
|
-
const publicKey = await
|
|
39409
|
+
const publicKey = await resolveArchivePublicKey({
|
|
39410
|
+
explicitPem: opts.publicKey,
|
|
39411
|
+
envPem: process.env.ARCHIVE_SIGNING_PUBLIC_KEY ?? process.env.STREAMS_SIGNING_PUBLIC_KEY,
|
|
39412
|
+
allowHostedApi: !isOssMode()
|
|
39413
|
+
});
|
|
39226
39414
|
const signature = checkSignature(reference.manifest, publicKey, false);
|
|
39227
39415
|
if (!signature.verified) {
|
|
39228
39416
|
printError(`Refusing to bootstrap from an untrusted archive: ${signature.reason}.`, {
|
|
@@ -39231,15 +39419,31 @@ Exit codes:
|
|
|
39231
39419
|
process.exit(BOOTSTRAP_EXIT.REFUSED);
|
|
39232
39420
|
}
|
|
39233
39421
|
const db = getDb4();
|
|
39234
|
-
const
|
|
39235
|
-
|
|
39236
|
-
|
|
39422
|
+
const progress = await db.selectFrom("index_progress").select("network").executeTakeFirst();
|
|
39423
|
+
const maxHeightRow = await db.selectFrom("blocks").select(({ fn }) => fn.max("height").as("max")).executeTakeFirst();
|
|
39424
|
+
const maxBlockHeight = maxHeightRow?.max === null || maxHeightRow?.max === undefined ? null : Number(maxHeightRow.max);
|
|
39425
|
+
const toBlock = opts.toBlock === undefined ? undefined : Number(opts.toBlock);
|
|
39426
|
+
const declared = (reference.manifest.partitions ?? []).filter((p2) => toBlock === undefined || p2.to_block <= toBlock);
|
|
39427
|
+
const resume = planTornImport({
|
|
39428
|
+
hasIndexProgress: !!progress,
|
|
39429
|
+
maxBlockHeight,
|
|
39430
|
+
partitions: declared
|
|
39431
|
+
});
|
|
39432
|
+
if (resume.action === "refuse") {
|
|
39433
|
+
printError(resume.reason, {
|
|
39237
39434
|
hint: "Bootstrap is for a fresh instance. To fix an existing one, use `sl repair`."
|
|
39238
39435
|
});
|
|
39239
39436
|
process.exit(BOOTSTRAP_EXIT.REFUSED);
|
|
39240
39437
|
}
|
|
39241
|
-
|
|
39242
|
-
|
|
39438
|
+
if (resume.action === "resume" && resume.truncateFrom !== null) {
|
|
39439
|
+
await sql3`DELETE FROM events WHERE block_height >= ${resume.truncateFrom}`.execute(db);
|
|
39440
|
+
await sql3`DELETE FROM transactions WHERE block_height >= ${resume.truncateFrom}`.execute(db);
|
|
39441
|
+
await sql3`DELETE FROM blocks WHERE height >= ${resume.truncateFrom}`.execute(db);
|
|
39442
|
+
note(`Resuming torn import: truncated from height ${resume.truncateFrom}.`);
|
|
39443
|
+
} else if (resume.action === "resume") {
|
|
39444
|
+
note(`Resuming import after sealed height ${resume.skipThrough.toLocaleString()}.`);
|
|
39445
|
+
}
|
|
39446
|
+
const partitions = declared.filter((p2) => resume.action === "fresh" || !partitionIsLoaded(p2, resume.skipThrough));
|
|
39243
39447
|
if (partitions.length === 0) {
|
|
39244
39448
|
printError("The archive has no partitions for that range.", {
|
|
39245
39449
|
hint: "Check --to-block against the manifest's coverage."
|
|
@@ -39364,138 +39568,217 @@ Exit codes:
|
|
|
39364
39568
|
}
|
|
39365
39569
|
});
|
|
39366
39570
|
}
|
|
39367
|
-
|
|
39368
|
-
|
|
39369
|
-
|
|
39370
|
-
|
|
39371
|
-
import {
|
|
39372
|
-
import {
|
|
39373
|
-
import {
|
|
39374
|
-
import {
|
|
39375
|
-
import {
|
|
39571
|
+
function registerBootstrapCommand(program2) {
|
|
39572
|
+
attachBootstrapCommand(program2.command("bootstrap").description("Restore chain history from a verified archive instead of syncing from genesis"));
|
|
39573
|
+
}
|
|
39574
|
+
// src/lib/instance-init.ts
|
|
39575
|
+
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
39576
|
+
import { existsSync as existsSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync7 } from "node:fs";
|
|
39577
|
+
import { resolve as resolve13 } from "node:path";
|
|
39578
|
+
import { generateEd25519KeyPair } from "@secondlayer/shared/crypto/ed25519";
|
|
39579
|
+
import { INSTANCE_NETWORKS } from "@secondlayer/shared/db/queries/instance";
|
|
39580
|
+
var INSTANCE_ENV_FILE = ".env.local";
|
|
39581
|
+
function parseInstanceNetwork(value) {
|
|
39582
|
+
const network = value.trim().toLowerCase();
|
|
39583
|
+
if (INSTANCE_NETWORKS.includes(network)) {
|
|
39584
|
+
return network;
|
|
39585
|
+
}
|
|
39586
|
+
throw new Error(`network must be mainnet, testnet, or devnet (got ${value})`);
|
|
39587
|
+
}
|
|
39588
|
+
function generateInstanceToken() {
|
|
39589
|
+
return randomBytes3(32).toString("hex");
|
|
39590
|
+
}
|
|
39591
|
+
function generateSecretsKey() {
|
|
39592
|
+
return randomBytes3(32).toString("hex");
|
|
39593
|
+
}
|
|
39594
|
+
function generateSigningPrivateKey() {
|
|
39595
|
+
return generateEd25519KeyPair().privateKeyPem;
|
|
39596
|
+
}
|
|
39597
|
+
function escapeEnvValue(value) {
|
|
39598
|
+
if (!/[\s#"']/.test(value) && !value.includes("\\"))
|
|
39599
|
+
return value;
|
|
39600
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n")}"`;
|
|
39601
|
+
}
|
|
39602
|
+
function unescapeEnvValue(raw) {
|
|
39603
|
+
let value = raw.trim();
|
|
39604
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
39605
|
+
value = value.slice(1, -1);
|
|
39606
|
+
}
|
|
39607
|
+
return value.replace(/\\n/g, `
|
|
39608
|
+
`).replace(/\\"/g, '"').replace(/\\\\/g, "\\");
|
|
39609
|
+
}
|
|
39610
|
+
function readEnvValue(envPath, key) {
|
|
39611
|
+
if (!existsSync8(envPath))
|
|
39612
|
+
return null;
|
|
39613
|
+
const match = readFileSync7(envPath, "utf8").match(new RegExp(`^${key}=(.*)$`, "m"));
|
|
39614
|
+
return match ? unescapeEnvValue(match[1]) : null;
|
|
39615
|
+
}
|
|
39616
|
+
function buildInstanceEnv(input4) {
|
|
39617
|
+
const token = input4.existing?.INSTANCE_TOKEN || input4.existing?.SL_API_KEY || generateInstanceToken();
|
|
39618
|
+
const secrets = input4.existing?.SECONDLAYER_SECRETS_KEY || generateSecretsKey();
|
|
39619
|
+
const signing = input4.existing?.STREAMS_SIGNING_PRIVATE_KEY || input4.existing?.SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY || generateSigningPrivateKey();
|
|
39620
|
+
const apiUrl = input4.apiUrl ?? "http://127.0.0.1:3800";
|
|
39621
|
+
const archiveKey = input4.existing?.ARCHIVE_SIGNING_PUBLIC_KEY;
|
|
39622
|
+
return {
|
|
39623
|
+
INSTANCE_MODE: "oss",
|
|
39624
|
+
STACKS_NETWORK: input4.network,
|
|
39625
|
+
INSTANCE_TOKEN: token,
|
|
39626
|
+
SECONDLAYER_SECRETS_KEY: secrets,
|
|
39627
|
+
STREAMS_SIGNING_PRIVATE_KEY: signing,
|
|
39628
|
+
SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY: signing,
|
|
39629
|
+
ALLOW_UNSIGNED_WEBHOOKS: "false",
|
|
39630
|
+
SL_API_URL: input4.existing?.SL_API_URL || apiUrl,
|
|
39631
|
+
SL_API_KEY: token,
|
|
39632
|
+
...archiveKey ? { ARCHIVE_SIGNING_PUBLIC_KEY: archiveKey } : {}
|
|
39633
|
+
};
|
|
39634
|
+
}
|
|
39635
|
+
function renderInstanceEnv(env2) {
|
|
39636
|
+
const lines2 = [
|
|
39637
|
+
"# Generated by sl init. Do not commit.",
|
|
39638
|
+
`INSTANCE_MODE=${env2.INSTANCE_MODE}`,
|
|
39639
|
+
`STACKS_NETWORK=${env2.STACKS_NETWORK}`,
|
|
39640
|
+
`INSTANCE_TOKEN=${env2.INSTANCE_TOKEN}`,
|
|
39641
|
+
`SECONDLAYER_SECRETS_KEY=${env2.SECONDLAYER_SECRETS_KEY}`,
|
|
39642
|
+
`STREAMS_SIGNING_PRIVATE_KEY=${escapeEnvValue(env2.STREAMS_SIGNING_PRIVATE_KEY)}`,
|
|
39643
|
+
`SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY=${escapeEnvValue(env2.SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY)}`,
|
|
39644
|
+
`ALLOW_UNSIGNED_WEBHOOKS=${env2.ALLOW_UNSIGNED_WEBHOOKS}`,
|
|
39645
|
+
`SL_API_URL=${env2.SL_API_URL}`,
|
|
39646
|
+
`SL_API_KEY=${env2.SL_API_KEY}`
|
|
39647
|
+
];
|
|
39648
|
+
if (env2.ARCHIVE_SIGNING_PUBLIC_KEY) {
|
|
39649
|
+
lines2.push(`ARCHIVE_SIGNING_PUBLIC_KEY=${escapeEnvValue(env2.ARCHIVE_SIGNING_PUBLIC_KEY)}`);
|
|
39650
|
+
}
|
|
39651
|
+
lines2.push("");
|
|
39652
|
+
return lines2.join(`
|
|
39653
|
+
`);
|
|
39654
|
+
}
|
|
39655
|
+
function writeInstanceEnv(cwd, env2, file = INSTANCE_ENV_FILE) {
|
|
39656
|
+
const path11 = resolve13(cwd, file);
|
|
39657
|
+
writeFileSync7(path11, renderInstanceEnv(env2), { mode: 384 });
|
|
39658
|
+
return path11;
|
|
39659
|
+
}
|
|
39660
|
+
function loadExistingInstanceEnv(cwd, file = INSTANCE_ENV_FILE) {
|
|
39661
|
+
const path11 = resolve13(cwd, file);
|
|
39662
|
+
return {
|
|
39663
|
+
INSTANCE_TOKEN: readEnvValue(path11, "INSTANCE_TOKEN"),
|
|
39664
|
+
SECONDLAYER_SECRETS_KEY: readEnvValue(path11, "SECONDLAYER_SECRETS_KEY"),
|
|
39665
|
+
STREAMS_SIGNING_PRIVATE_KEY: readEnvValue(path11, "STREAMS_SIGNING_PRIVATE_KEY"),
|
|
39666
|
+
SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY: readEnvValue(path11, "SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY"),
|
|
39667
|
+
SL_API_URL: readEnvValue(path11, "SL_API_URL"),
|
|
39668
|
+
SL_API_KEY: readEnvValue(path11, "SL_API_KEY"),
|
|
39669
|
+
ARCHIVE_SIGNING_PUBLIC_KEY: readEnvValue(path11, "ARCHIVE_SIGNING_PUBLIC_KEY")
|
|
39670
|
+
};
|
|
39671
|
+
}
|
|
39672
|
+
|
|
39673
|
+
// src/commands/init.ts
|
|
39376
39674
|
init_output();
|
|
39377
|
-
|
|
39378
|
-
|
|
39379
|
-
|
|
39380
|
-
|
|
39381
|
-
|
|
39382
|
-
|
|
39383
|
-
|
|
39384
|
-
|
|
39385
|
-
|
|
39675
|
+
function registerInitCommand(program2) {
|
|
39676
|
+
program2.command("init").description("Write local env (token, secrets key, webhook signing key). Idempotent.").option("--network <network>", "mainnet, testnet, or devnet", process.env.STACKS_NETWORK ?? "mainnet").option("--api-url <url>", "Local API URL", "http://127.0.0.1:3800").option("--force", "Overwrite generated values even if .env.local exists").action((opts) => {
|
|
39677
|
+
const network = parseInstanceNetwork(opts.network);
|
|
39678
|
+
const existing = opts.force ? {} : loadExistingInstanceEnv(process.cwd());
|
|
39679
|
+
const env2 = buildInstanceEnv({
|
|
39680
|
+
network,
|
|
39681
|
+
existing,
|
|
39682
|
+
apiUrl: opts.apiUrl
|
|
39683
|
+
});
|
|
39684
|
+
const path11 = writeInstanceEnv(process.cwd(), env2);
|
|
39685
|
+
success(`Wrote ${INSTANCE_ENV_FILE}`);
|
|
39686
|
+
note(formatKeyValue([
|
|
39687
|
+
["network", env2.STACKS_NETWORK],
|
|
39688
|
+
["api", env2.SL_API_URL],
|
|
39689
|
+
["env", path11]
|
|
39690
|
+
]));
|
|
39691
|
+
note("Export these or pass --env-file to compose. Do not commit the file.");
|
|
39692
|
+
});
|
|
39693
|
+
}
|
|
39694
|
+
// src/lib/observer-stanza.ts
|
|
39695
|
+
var OBSERVER_MODES = ["indexer", "signer-shared"];
|
|
39696
|
+
var RECOVERY_SOURCES = ["journal", "archive"];
|
|
39697
|
+
|
|
39698
|
+
class UnsupportedObserverError extends Error {
|
|
39699
|
+
name = "UnsupportedObserverError";
|
|
39700
|
+
}
|
|
39701
|
+
function parseObserverMode(value) {
|
|
39702
|
+
const mode = value.trim().toLowerCase();
|
|
39703
|
+
if (OBSERVER_MODES.includes(mode)) {
|
|
39704
|
+
return mode;
|
|
39386
39705
|
}
|
|
39387
|
-
|
|
39388
|
-
|
|
39389
|
-
|
|
39390
|
-
|
|
39706
|
+
throw new UnsupportedObserverError(`observer mode must be indexer or signer-shared (got ${value})`);
|
|
39707
|
+
}
|
|
39708
|
+
function parseRecoverySource(value) {
|
|
39709
|
+
const source = value.trim().toLowerCase();
|
|
39710
|
+
if (RECOVERY_SOURCES.includes(source)) {
|
|
39711
|
+
return source;
|
|
39391
39712
|
}
|
|
39392
|
-
|
|
39713
|
+
throw new UnsupportedObserverError(`recovery source must be journal or archive (got ${value})`);
|
|
39393
39714
|
}
|
|
39394
|
-
|
|
39395
|
-
const
|
|
39396
|
-
|
|
39397
|
-
|
|
39398
|
-
const height = info2.burn_block_height;
|
|
39399
|
-
if (typeof height !== "number") {
|
|
39400
|
-
throw new Error("node did not report a burn block height");
|
|
39715
|
+
function parseObserverEndpoint(value) {
|
|
39716
|
+
const trimmed = value.trim();
|
|
39717
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed) || trimmed.startsWith("unix:")) {
|
|
39718
|
+
throw new UnsupportedObserverError(`observer endpoint must be host:port, not a URL (${value})`);
|
|
39401
39719
|
}
|
|
39402
|
-
const
|
|
39403
|
-
if (!
|
|
39404
|
-
throw new
|
|
39720
|
+
const match = trimmed.match(/^([a-zA-Z0-9._-]+):(\d+)$/);
|
|
39721
|
+
if (!match) {
|
|
39722
|
+
throw new UnsupportedObserverError(`observer endpoint must be host:port (got ${value})`);
|
|
39405
39723
|
}
|
|
39406
|
-
|
|
39724
|
+
const port = Number(match[2]);
|
|
39725
|
+
if (port < 1 || port > 65535) {
|
|
39726
|
+
throw new UnsupportedObserverError(`observer port out of range (${port})`);
|
|
39727
|
+
}
|
|
39728
|
+
return trimmed;
|
|
39407
39729
|
}
|
|
39408
|
-
|
|
39409
|
-
|
|
39410
|
-
|
|
39411
|
-
const
|
|
39412
|
-
if (
|
|
39413
|
-
throw new
|
|
39730
|
+
var LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "::1"]);
|
|
39731
|
+
function validateObserverStanza(input4) {
|
|
39732
|
+
const endpoint = parseObserverEndpoint(input4.endpoint);
|
|
39733
|
+
const host = endpoint.slice(0, endpoint.lastIndexOf(":"));
|
|
39734
|
+
if (input4.network !== "devnet" && LOOPBACK_HOSTS.has(host.toLowerCase())) {
|
|
39735
|
+
throw new UnsupportedObserverError(`loopback observer endpoint is not container-visible on ${input4.network}; use a docker DNS name such as indexer:3700`);
|
|
39414
39736
|
}
|
|
39415
|
-
|
|
39737
|
+
if (input4.mode === "signer-shared" && !input4.recovery) {
|
|
39738
|
+
throw new UnsupportedObserverError("signer-shared mode requires --recovery journal or --recovery archive");
|
|
39739
|
+
}
|
|
39740
|
+
return { endpoint };
|
|
39741
|
+
}
|
|
39742
|
+
function renderObserverStanza(input4) {
|
|
39743
|
+
const { endpoint } = validateObserverStanza(input4);
|
|
39744
|
+
const indexer = input4.mode === "indexer";
|
|
39745
|
+
const timeoutMs = indexer ? 2000 : 500;
|
|
39746
|
+
const disableRetries = !indexer;
|
|
39747
|
+
const comment = indexer ? "# Pure indexer: retry delivery. A slow observer can stall the node." : "# Signer-shared: do not retry. Missed blocks refill from the journal/archive.";
|
|
39748
|
+
return [
|
|
39749
|
+
comment,
|
|
39750
|
+
"[[events_observer]]",
|
|
39751
|
+
`endpoint = "${endpoint}"`,
|
|
39752
|
+
'events_keys = ["*"]',
|
|
39753
|
+
`timeout_ms = ${timeoutMs}`,
|
|
39754
|
+
`disable_retries = ${disableRetries}`,
|
|
39755
|
+
""
|
|
39756
|
+
].join(`
|
|
39757
|
+
`);
|
|
39758
|
+
}
|
|
39759
|
+
function defaultObserverEndpoint(network) {
|
|
39760
|
+
if (network === "devnet")
|
|
39761
|
+
return "127.0.0.1:3700";
|
|
39762
|
+
return "indexer:3700";
|
|
39416
39763
|
}
|
|
39417
|
-
function registerFaucetCommand(program2) {
|
|
39418
|
-
const faucet = program2.command("faucet").description("Mint test assets on a local devnet");
|
|
39419
|
-
faucet.command("sbtc").description("Mint mock sBTC to an address on your local devnet").requiredOption("--to <address>", "recipient Stacks address").option("--amount <sats>", `amount in sats (default ${DEFAULT_AMOUNT_SATS}, minimum ${DUST_LIMIT_SATS})`).option("--contract <id>", "sBTC deposit contract (default: <deployer>.sbtc-deposit)").option("--rpc-url <url>", "devnet node RPC (default http://localhost:3999)").option("--json", "Output as JSON").addHelpText("after", `
|
|
39420
|
-
Examples:
|
|
39421
|
-
$ sl faucet sbtc --to ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM
|
|
39422
|
-
$ sl faucet sbtc --to ST2... --amount 5000000
|
|
39423
39764
|
|
|
39424
|
-
|
|
39425
|
-
|
|
39426
|
-
|
|
39427
|
-
|
|
39428
|
-
|
|
39429
|
-
|
|
39430
|
-
|
|
39431
|
-
|
|
39432
|
-
|
|
39433
|
-
|
|
39434
|
-
}
|
|
39435
|
-
const rpcUrl = opts.rpcUrl ?? "http://localhost:3999";
|
|
39436
|
-
const amount = parseAmount(opts.amount);
|
|
39437
|
-
const projectDir = findClarinetProject(process.cwd());
|
|
39438
|
-
if (!projectDir) {
|
|
39439
|
-
printError("No Clarinet project found.", {
|
|
39440
|
-
hint: "Run this from a project containing Clarinet.toml, or pass --contract explicitly."
|
|
39441
|
-
});
|
|
39442
|
-
process.exit(2);
|
|
39443
|
-
}
|
|
39444
|
-
const mnemonic = await readDeployerMnemonic(projectDir);
|
|
39445
|
-
const account = mnemonicToAccount(mnemonic, {
|
|
39446
|
-
addressVersion: devnet.addressVersion.singleSig
|
|
39447
|
-
});
|
|
39448
|
-
const contract = opts.contract ?? `${account.address}.sbtc-deposit`;
|
|
39449
|
-
const anchor = await readBurnAnchor(rpcUrl);
|
|
39450
|
-
const client3 = createWalletClient({
|
|
39451
|
-
account,
|
|
39452
|
-
chain: devnet,
|
|
39453
|
-
transport: http3(rpcUrl)
|
|
39454
|
-
});
|
|
39455
|
-
const depositTxid = randomBytes2(32).toString("hex");
|
|
39456
|
-
const sweepTxid = randomBytes2(32).toString("hex");
|
|
39457
|
-
note(` minting ${amount} sats to ${opts.to}`);
|
|
39458
|
-
note(` via ${contract} as signer ${account.address}`);
|
|
39459
|
-
const txid = await client3.callContract({
|
|
39460
|
-
contract,
|
|
39461
|
-
functionName: "complete-deposit-wrapper",
|
|
39462
|
-
functionArgs: [
|
|
39463
|
-
Cl.bufferFromHex(depositTxid),
|
|
39464
|
-
Cl.uint(0),
|
|
39465
|
-
Cl.uint(amount),
|
|
39466
|
-
Cl.principal(opts.to),
|
|
39467
|
-
Cl.bufferFromHex(anchor.hash),
|
|
39468
|
-
Cl.uint(anchor.height),
|
|
39469
|
-
Cl.bufferFromHex(sweepTxid)
|
|
39470
|
-
],
|
|
39471
|
-
postConditionMode: "allow"
|
|
39472
|
-
});
|
|
39473
|
-
output({
|
|
39474
|
-
json: opts.json,
|
|
39475
|
-
data: {
|
|
39476
|
-
txid,
|
|
39477
|
-
recipient: opts.to,
|
|
39478
|
-
amount_sats: amount.toString(),
|
|
39479
|
-
contract,
|
|
39480
|
-
burn_height: anchor.height
|
|
39481
|
-
},
|
|
39482
|
-
human: () => {
|
|
39483
|
-
success(`Minted ${amount} sats of mock sBTC to ${opts.to}.`);
|
|
39484
|
-
note(` tx ${txid}`);
|
|
39485
|
-
console.error(dim(" Mock sBTC on your devnet — not the canonical asset."));
|
|
39486
|
-
}
|
|
39487
|
-
});
|
|
39488
|
-
} catch (err) {
|
|
39489
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
39490
|
-
const hint = /ECONNREFUSED|fetch failed/.test(message) ? "Is your devnet running? Start it with `clarinet devnet start`." : /NoSuchContract|does not exist/.test(message) ? "Deploy the sBTC contract set to your devnet first, or pass --contract." : "Check that your Clarinet deployer matches the sBTC contract deployer.";
|
|
39491
|
-
printError(message, { hint });
|
|
39492
|
-
process.exit(1);
|
|
39765
|
+
// src/commands/observer.ts
|
|
39766
|
+
init_output();
|
|
39767
|
+
function registerObserverCommand(program2) {
|
|
39768
|
+
program2.command("observer").description("Print the Stacks events_observer stanza for this node mode").option("--mode <mode>", "indexer (retry) or signer-shared (no retry)", "indexer").option("--endpoint <host:port>", "Observer callback the node will POST to").option("--recovery <source>", "Required for signer-shared: journal or archive").option("--network <network>", "mainnet, testnet, or devnet", process.env.STACKS_NETWORK ?? "mainnet").action((opts) => {
|
|
39769
|
+
const mode = parseObserverMode(opts.mode);
|
|
39770
|
+
const network = parseInstanceNetwork(opts.network);
|
|
39771
|
+
const endpoint = opts.endpoint ?? defaultObserverEndpoint(network);
|
|
39772
|
+
const recovery = opts.recovery ? parseRecoverySource(opts.recovery) : undefined;
|
|
39773
|
+
if (mode === "signer-shared") {
|
|
39774
|
+
note("Signer-shared nodes skip observer retries. Pair with a journal or archive for completeness.");
|
|
39493
39775
|
}
|
|
39776
|
+
writeData(renderObserverStanza({ mode, endpoint, network, recovery }));
|
|
39494
39777
|
});
|
|
39495
39778
|
}
|
|
39496
39779
|
// src/cli.ts
|
|
39497
39780
|
var { version } = package_default;
|
|
39498
|
-
program.name("secondlayer").alias("sl").description("
|
|
39781
|
+
program.name("secondlayer").alias("sl").description("Secondlayer CLI — self-hosted Stacks data runtime").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 `sl --help` to see available commands)");
|
|
39499
39782
|
program.hook("preAction", (thisCommand) => {
|
|
39500
39783
|
const { network, apiKey, apiUrl } = thisCommand.opts();
|
|
39501
39784
|
if (network)
|
|
@@ -39507,16 +39790,15 @@ program.hook("preAction", (thisCommand) => {
|
|
|
39507
39790
|
});
|
|
39508
39791
|
program.addHelpText("after", `
|
|
39509
39792
|
Quickstart:
|
|
39510
|
-
$ sl
|
|
39793
|
+
$ sl init --network mainnet
|
|
39794
|
+
$ sl bootstrap --against <manifest>
|
|
39511
39795
|
$ sl subgraphs create my-watcher --template sip-010-balances
|
|
39512
39796
|
$ sl subgraphs deploy subgraphs/my-watcher.ts
|
|
39513
|
-
$ sl subgraphs status my-watcher
|
|
39514
39797
|
`);
|
|
39515
39798
|
program.commandsGroup("Getting started:");
|
|
39516
|
-
program
|
|
39517
|
-
|
|
39518
|
-
|
|
39519
|
-
});
|
|
39799
|
+
registerInitCommand(program);
|
|
39800
|
+
registerBootstrapCommand(program);
|
|
39801
|
+
registerObserverCommand(program);
|
|
39520
39802
|
registerLoginCommand(program);
|
|
39521
39803
|
registerLogoutCommand(program);
|
|
39522
39804
|
registerWhoamiCommand(program);
|
|
@@ -39544,13 +39826,11 @@ Examples:
|
|
|
39544
39826
|
program.commandsGroup("Local development:");
|
|
39545
39827
|
registerLocalCommand(program);
|
|
39546
39828
|
registerDevnetCommand(program);
|
|
39547
|
-
registerFaucetCommand(program);
|
|
39548
39829
|
program.commandsGroup("Diagnostics:");
|
|
39549
39830
|
registerStatusCommand(program);
|
|
39550
39831
|
registerDoctorCommand(program);
|
|
39551
39832
|
registerConfigCommand(program);
|
|
39552
39833
|
registerContextCommand(program);
|
|
39553
|
-
registerBootstrapCommand(program);
|
|
39554
39834
|
registerVerifyCommand(program);
|
|
39555
39835
|
registerRepairCommand(program);
|
|
39556
39836
|
program.commandsGroup("Account:");
|
|
@@ -39558,5 +39838,5 @@ registerAccountCommand(program);
|
|
|
39558
39838
|
registerKeysCommand(program);
|
|
39559
39839
|
program.parse();
|
|
39560
39840
|
|
|
39561
|
-
//# debugId=
|
|
39841
|
+
//# debugId=8F8395688EA7983464756E2164756E21
|
|
39562
39842
|
//# sourceMappingURL=cli.js.map
|