@secondlayer/cli 9.8.0 → 9.9.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/dist/cli.js CHANGED
@@ -34817,7 +34817,7 @@ var {
34817
34817
  // package.json
34818
34818
  var package_default = {
34819
34819
  name: "@secondlayer/cli",
34820
- version: "9.8.0",
34820
+ version: "9.9.0",
34821
34821
  repository: {
34822
34822
  type: "git",
34823
34823
  url: "git+https://github.com/ryanwaits/secondlayer.git",
@@ -34861,8 +34861,8 @@ var package_default = {
34861
34861
  "@secondlayer/bundler": "^0.4.1",
34862
34862
  "@secondlayer/scaffold": "^1.5.1",
34863
34863
  "@secondlayer/sdk": "^6.51.0",
34864
- "@secondlayer/shared": "^7.7.0",
34865
- "@secondlayer/stacks": "^3.2.0",
34864
+ "@secondlayer/shared": "^7.8.0",
34865
+ "@secondlayer/stacks": "^3.3.1",
34866
34866
  "@secondlayer/subgraphs": "^3.26.4",
34867
34867
  esbuild: "^0.27.4",
34868
34868
  "smol-toml": "^1.6.1",
@@ -38638,6 +38638,11 @@ import { getDb as getDb2 } from "@secondlayer/shared/db";
38638
38638
  import { createHash as createHash2 } from "node:crypto";
38639
38639
  import { readFile as readFile4 } from "node:fs/promises";
38640
38640
  import { dirname as dirname16, join as join13, resolve as resolve12 } from "node:path";
38641
+ import {
38642
+ checkKeyTrust,
38643
+ publicKeyFor,
38644
+ verifyRegistry
38645
+ } from "@secondlayer/shared/archive/key-registry";
38641
38646
  import { verifyStreamsBulkManifestSignature } from "@secondlayer/shared/streams-bulk-manifest";
38642
38647
  function isLatestPointer(value) {
38643
38648
  const pointer = value;
@@ -39143,7 +39148,7 @@ import {
39143
39148
  writeRowsToCopyStream
39144
39149
  } from "@secondlayer/shared/archive/copy-loader";
39145
39150
  import { computeRangeDigest as computeRangeDigest3 } from "@secondlayer/shared/archive/range-digest";
39146
- import { getDb as getDb4, getRawClient } from "@secondlayer/shared/db";
39151
+ import { getDb as getDb4, getRawClient, sql as sql3 } from "@secondlayer/shared/db";
39147
39152
  init_output();
39148
39153
  var BOOTSTRAP_EXIT = {
39149
39154
  OK: 0,
@@ -39152,6 +39157,20 @@ var BOOTSTRAP_EXIT = {
39152
39157
  };
39153
39158
  var BLOCKS_PER_DAY_ESTIMATE = 5400;
39154
39159
  var RESTORE_ROWS_PER_SECOND = 30000;
39160
+ async function readNodeTip() {
39161
+ const url = process.env.STACKS_NODE_RPC_URL ?? "http://localhost:20443";
39162
+ try {
39163
+ const response2 = await fetch(`${url.replace(/\/$/, "")}/v2/info`, {
39164
+ signal: AbortSignal.timeout(5000)
39165
+ });
39166
+ if (!response2.ok)
39167
+ return null;
39168
+ const info2 = await response2.json();
39169
+ return typeof info2.stacks_tip_height === "number" ? info2.stacks_tip_height : null;
39170
+ } catch {
39171
+ return null;
39172
+ }
39173
+ }
39155
39174
  function formatDuration2(seconds2) {
39156
39175
  if (seconds2 < 90)
39157
39176
  return `${Math.round(seconds2)}s`;
@@ -39256,6 +39275,7 @@ Exit codes:
39256
39275
  process.exit(BOOTSTRAP_EXIT.REFUSED);
39257
39276
  }
39258
39277
  }
39278
+ const nodeTipAtStart = await readNodeTip();
39259
39279
  const order = ["blocks", "transactions", "events"];
39260
39280
  const rawClient = getRawClient("source");
39261
39281
  const loaded = { blocks: 0, transactions: 0, events: 0 };
@@ -39279,6 +39299,24 @@ Exit codes:
39279
39299
  divergent++;
39280
39300
  }
39281
39301
  }
39302
+ const network = process.env.STACKS_NETWORK ?? "mainnet";
39303
+ await sql3`
39304
+ INSERT INTO index_progress (
39305
+ network, last_indexed_block, last_contiguous_block,
39306
+ highest_seen_block, updated_at
39307
+ ) VALUES (
39308
+ ${network}, ${tipHeight}, ${tipHeight}, ${tipHeight}, NOW()
39309
+ )
39310
+ ON CONFLICT (network) DO UPDATE SET
39311
+ last_indexed_block = GREATEST(index_progress.last_indexed_block, EXCLUDED.last_indexed_block),
39312
+ last_contiguous_block = GREATEST(index_progress.last_contiguous_block, EXCLUDED.last_contiguous_block),
39313
+ highest_seen_block = GREATEST(index_progress.highest_seen_block, EXCLUDED.highest_seen_block),
39314
+ updated_at = NOW()
39315
+ `.execute(db);
39316
+ const seam = nodeTipAtStart !== null && nodeTipAtStart > tipHeight ? {
39317
+ nodeTip: nodeTipAtStart,
39318
+ gap: nodeTipAtStart - tipHeight
39319
+ } : null;
39282
39320
  const elapsed = (Date.now() - startedAt) / 1000;
39283
39321
  const report = {
39284
39322
  status: divergent === 0 ? "restored" : "divergent",
@@ -39287,7 +39325,10 @@ Exit codes:
39287
39325
  rows: loaded,
39288
39326
  ranges_verified: referenceDigests.length,
39289
39327
  divergent_ranges: divergent,
39290
- elapsed_seconds: Math.round(elapsed)
39328
+ elapsed_seconds: Math.round(elapsed),
39329
+ resume_from: tipHeight + 1,
39330
+ node_tip_at_start: nodeTipAtStart,
39331
+ catch_up_blocks: seam?.gap ?? null
39291
39332
  };
39292
39333
  output({
39293
39334
  json: opts.json,
@@ -39301,7 +39342,13 @@ Exit codes:
39301
39342
  warn(" archive published no block digests — restore is unverified");
39302
39343
  }
39303
39344
  console.error("");
39304
- console.error(dim(`Your instance holds history through ${tipHeight.toLocaleString()}. Start the indexer to continue from there.`));
39345
+ console.error(dim(`Your instance holds history through ${tipHeight.toLocaleString()} and will resume at ${(tipHeight + 1).toLocaleString()}.`));
39346
+ if (seam) {
39347
+ console.error(dim(`The chain advanced to ${seam.nodeTip.toLocaleString()} while restoring — ${seam.gap.toLocaleString()} blocks to catch up.`));
39348
+ console.error(dim(" Start the indexer; it backfills that range before following the tip."));
39349
+ } else {
39350
+ console.error(dim(" Start the indexer to continue from there. (Node unreachable — catch-up range unknown.)"));
39351
+ }
39305
39352
  } else {
39306
39353
  warn(`Restored ${loaded.blocks.toLocaleString()} blocks but ${divergent} ranges do not match the archive.`);
39307
39354
  console.error(dim(" Investigate with: sl verify --against <manifest>"));
@@ -39317,6 +39364,135 @@ Exit codes:
39317
39364
  }
39318
39365
  });
39319
39366
  }
39367
+ // src/commands/faucet.ts
39368
+ import { randomBytes as randomBytes2 } from "node:crypto";
39369
+ import { readFile as readFile5 } from "node:fs/promises";
39370
+ import { join as join16 } from "node:path";
39371
+ import { http as http3, createWalletClient } from "@secondlayer/stacks";
39372
+ import { mnemonicToAccount } from "@secondlayer/stacks/accounts";
39373
+ import { devnet } from "@secondlayer/stacks/chains";
39374
+ import { Cl } from "@secondlayer/stacks/clarity";
39375
+ import { parse as parseToml2 } from "smol-toml";
39376
+ init_output();
39377
+ var DUST_LIMIT_SATS = 546n;
39378
+ var DEFAULT_AMOUNT_SATS = 1000000n;
39379
+ async function readDeployerMnemonic(projectDir) {
39380
+ const path11 = join16(projectDir, "settings", "Devnet.toml");
39381
+ let raw;
39382
+ try {
39383
+ raw = await readFile5(path11, "utf8");
39384
+ } catch {
39385
+ throw new Error(`could not read ${path11}`);
39386
+ }
39387
+ const parsed = parseToml2(raw);
39388
+ const mnemonic = parsed.accounts?.deployer?.mnemonic;
39389
+ if (!mnemonic) {
39390
+ throw new Error(`no accounts.deployer.mnemonic in ${path11}`);
39391
+ }
39392
+ return mnemonic;
39393
+ }
39394
+ async function readBurnAnchor(rpcUrl) {
39395
+ const info2 = await (await fetch(`${rpcUrl.replace(/\/$/, "")}/v2/info`, {
39396
+ signal: AbortSignal.timeout(1e4)
39397
+ })).json();
39398
+ const height = info2.burn_block_height;
39399
+ if (typeof height !== "number") {
39400
+ throw new Error("node did not report a burn block height");
39401
+ }
39402
+ const burn = await (await fetch(`${rpcUrl.replace(/\/$/, "")}/extended/v2/burn-blocks/${height}`, { signal: AbortSignal.timeout(1e4) })).json();
39403
+ if (!burn.burn_block_hash) {
39404
+ throw new Error(`node did not report a hash for burn block ${height}`);
39405
+ }
39406
+ return { height, hash: burn.burn_block_hash.replace(/^0x/, "") };
39407
+ }
39408
+ function parseAmount(value) {
39409
+ if (value === undefined)
39410
+ return DEFAULT_AMOUNT_SATS;
39411
+ const parsed = BigInt(value);
39412
+ if (parsed < DUST_LIMIT_SATS) {
39413
+ throw new Error(`--amount must be at least ${DUST_LIMIT_SATS} sats (the protocol dust limit), got ${parsed}`);
39414
+ }
39415
+ return parsed;
39416
+ }
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
+
39424
+ Requires a devnet running with the sBTC contract set deployed by your
39425
+ Clarinet deployer — that deployer is the signer set on your own chain, which
39426
+ is what makes minting possible without Bitcoin.`).action(async (opts) => {
39427
+ try {
39428
+ const network = process.env.STACKS_NETWORK ?? "devnet";
39429
+ if (network !== "devnet" && network !== "local") {
39430
+ printError(`sl faucet only works on devnet, not ${network}.`, {
39431
+ hint: "Mock sBTC is mintable because you deployed the contracts. On testnet or mainnet the signer set is not yours."
39432
+ });
39433
+ process.exit(2);
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);
39493
+ }
39494
+ });
39495
+ }
39320
39496
  // src/cli.ts
39321
39497
  var { version } = package_default;
39322
39498
  program.name("secondlayer").alias("sl").description("SecondLayer CLI — dedicated Stacks indexing + real-time subgraphs").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)");
@@ -39368,6 +39544,7 @@ Examples:
39368
39544
  program.commandsGroup("Local development:");
39369
39545
  registerLocalCommand(program);
39370
39546
  registerDevnetCommand(program);
39547
+ registerFaucetCommand(program);
39371
39548
  program.commandsGroup("Diagnostics:");
39372
39549
  registerStatusCommand(program);
39373
39550
  registerDoctorCommand(program);
@@ -39381,5 +39558,5 @@ registerAccountCommand(program);
39381
39558
  registerKeysCommand(program);
39382
39559
  program.parse();
39383
39560
 
39384
- //# debugId=C0418C213027DA8564756E2164756E21
39561
+ //# debugId=F913417C10273C0264756E2164756E21
39385
39562
  //# sourceMappingURL=cli.js.map