@secondlayer/cli 11.0.0 → 12.1.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 +5 -6
- package/dist/cli.js +405 -407
- package/dist/cli.js.map +10 -8
- package/package.json +7 -7
package/dist/cli.js
CHANGED
|
@@ -10414,9 +10414,9 @@ function lowercaseKeys(object) {
|
|
|
10414
10414
|
|
|
10415
10415
|
// ../../node_modules/responselike/index.js
|
|
10416
10416
|
import { Readable as ReadableStream2 } from "node:stream";
|
|
10417
|
-
var
|
|
10417
|
+
var Response2;
|
|
10418
10418
|
var init_responselike = __esm(() => {
|
|
10419
|
-
|
|
10419
|
+
Response2 = class Response2 extends ReadableStream2 {
|
|
10420
10420
|
statusCode;
|
|
10421
10421
|
headers;
|
|
10422
10422
|
body;
|
|
@@ -10854,7 +10854,7 @@ class CacheableRequest {
|
|
|
10854
10854
|
response.once("end", resolve);
|
|
10855
10855
|
});
|
|
10856
10856
|
const headers = convertHeaders(revalidatedPolicy.policy.responseHeaders());
|
|
10857
|
-
response = new
|
|
10857
|
+
response = new Response2({ statusCode: revalidate.statusCode, headers, body: revalidate.body, url: revalidate.url });
|
|
10858
10858
|
response.cachePolicy = revalidatedPolicy.policy;
|
|
10859
10859
|
response.fromCache = true;
|
|
10860
10860
|
}
|
|
@@ -10930,7 +10930,7 @@ class CacheableRequest {
|
|
|
10930
10930
|
const policy = import_http_cache_semantics.default.fromObject(cacheEntry.cachePolicy);
|
|
10931
10931
|
if (policy.satisfiesWithoutRevalidation(options_) && !options_.forceRefresh) {
|
|
10932
10932
|
const headers = convertHeaders(policy.responseHeaders());
|
|
10933
|
-
const response = new
|
|
10933
|
+
const response = new Response2({ statusCode: cacheEntry.statusCode, headers, body: cacheEntry.body, url: cacheEntry.url });
|
|
10934
10934
|
response.cachePolicy = policy;
|
|
10935
10935
|
response.fromCache = true;
|
|
10936
10936
|
ee.emit("response", response);
|
|
@@ -28878,23 +28878,8 @@ var init_require_auth = __esm(() => {
|
|
|
28878
28878
|
});
|
|
28879
28879
|
|
|
28880
28880
|
// src/templates/subgraph.ts
|
|
28881
|
-
function
|
|
28882
|
-
|
|
28883
|
-
switch (slug) {
|
|
28884
|
-
case "sip-010-balances":
|
|
28885
|
-
return sip010Balances(name);
|
|
28886
|
-
case "sbtc-flows":
|
|
28887
|
-
return sbtcFlows(name);
|
|
28888
|
-
case "pox-stacking":
|
|
28889
|
-
return poxStacking(name);
|
|
28890
|
-
case "bns-names":
|
|
28891
|
-
return bnsNames(name);
|
|
28892
|
-
default:
|
|
28893
|
-
return basic(name);
|
|
28894
|
-
}
|
|
28895
|
-
})();
|
|
28896
|
-
return `${nextStepsHeader(name)}
|
|
28897
|
-
${body}`;
|
|
28881
|
+
function generateSubgraphStarter(name) {
|
|
28882
|
+
return `${nextStepsHeader(name)}${starter(name)}`;
|
|
28898
28883
|
}
|
|
28899
28884
|
function nextStepsHeader(name) {
|
|
28900
28885
|
return `// ───────────────────────────────────────────────────────────────────
|
|
@@ -28903,20 +28888,19 @@ function nextStepsHeader(name) {
|
|
|
28903
28888
|
// 1. Edit the source filter + schema below to match what you want to track.
|
|
28904
28889
|
// 2. Edit the handler at the bottom — it runs once per matching event.
|
|
28905
28890
|
// 3. Deploy: secondlayer subgraphs deploy ${name}.ts
|
|
28906
|
-
// (You'll be prompted to log in if this is your first remote deploy.)
|
|
28907
28891
|
// 4. Wait for sync: secondlayer subgraphs status ${name}
|
|
28908
|
-
//
|
|
28909
|
-
//
|
|
28892
|
+
// Backfill from genesis can take an hour or more depending on your
|
|
28893
|
+
// filter scope. Use --start-block to skip ahead.
|
|
28910
28894
|
// 5. Query: secondlayer subgraphs query ${name} <table-name>
|
|
28911
|
-
// Or hit the
|
|
28895
|
+
// Or hit the REST endpoint listed in the deploy output.
|
|
28912
28896
|
//
|
|
28913
|
-
//
|
|
28914
|
-
//
|
|
28897
|
+
// Prefer generating from a real contract:
|
|
28898
|
+
// secondlayer subgraphs create ${name} --from-contract SP....my-contract
|
|
28915
28899
|
// ───────────────────────────────────────────────────────────────────
|
|
28916
28900
|
|
|
28917
28901
|
`;
|
|
28918
28902
|
}
|
|
28919
|
-
function
|
|
28903
|
+
function starter(name) {
|
|
28920
28904
|
return `import { defineSubgraph } from "@secondlayer/subgraphs";
|
|
28921
28905
|
|
|
28922
28906
|
export default defineSubgraph({
|
|
@@ -28924,7 +28908,7 @@ export default defineSubgraph({
|
|
|
28924
28908
|
version: "1.0.0",
|
|
28925
28909
|
description: "TODO: describe what this subgraph tracks",
|
|
28926
28910
|
|
|
28927
|
-
// Sources define what
|
|
28911
|
+
// Sources define what chain data this subgraph processes.
|
|
28928
28912
|
// Each source is named — the name becomes the handler key.
|
|
28929
28913
|
//
|
|
28930
28914
|
// Filter types:
|
|
@@ -28969,345 +28953,6 @@ export default defineSubgraph({
|
|
|
28969
28953
|
});
|
|
28970
28954
|
`;
|
|
28971
28955
|
}
|
|
28972
|
-
function sip010Balances(name) {
|
|
28973
|
-
return `import { defineSchema, defineSubgraph, readContractAt, type TypedSubgraphContext } from "@secondlayer/subgraphs";
|
|
28974
|
-
|
|
28975
|
-
/**
|
|
28976
|
-
* Track SIP-010 token balances per (asset_identifier, holder).
|
|
28977
|
-
* Mirrors the shape of the curated sBTC token-events view
|
|
28978
|
-
* surface — but works for ANY SIP-010 token. Constrain to a single
|
|
28979
|
-
* token by adding \`assetIdentifier: "SP...token::token-name"\` to each
|
|
28980
|
-
* source filter.
|
|
28981
|
-
*
|
|
28982
|
-
* Balances are moved with \`ctx.increment\`, which applies a DELTA in one
|
|
28983
|
-
* atomic statement. That matters: a read-modify-write (findOne → compute →
|
|
28984
|
-
* upsert) loses concurrent updates to the same row, and it is not replay-safe
|
|
28985
|
-
* under \`backfillMode: "concurrent"\`. Deltas commute, so order doesn't
|
|
28986
|
-
* matter and a reorg rewind reverses cleanly via the journal.
|
|
28987
|
-
*
|
|
28988
|
-
* Token metadata (decimals/symbol) comes from the token contract itself via
|
|
28989
|
-
* \`readContractAt\`, so \`amount\` stops being a bare integer nobody can
|
|
28990
|
-
* render. The read is pinned to the block being processed — a handler stays a
|
|
28991
|
-
* pure function of its block — and \`cache: "contract-constant"\` says these
|
|
28992
|
-
* two values can never change for a given token, so each is fetched ONCE
|
|
28993
|
-
* instead of once per block.
|
|
28994
|
-
*
|
|
28995
|
-
* Query examples once deployed:
|
|
28996
|
-
* GET /v1/subgraphs/${name}/balances?_search=SP1...
|
|
28997
|
-
* GET /v1/subgraphs/${name}/balances?holder=SP1...
|
|
28998
|
-
* GET /v1/subgraphs/${name}/tokens
|
|
28999
|
-
*/
|
|
29000
|
-
|
|
29001
|
-
/** The two SIP-010 read-only functions this subgraph calls. */
|
|
29002
|
-
const SIP010_ABI = {
|
|
29003
|
-
functions: [
|
|
29004
|
-
{ name: "get-decimals", access: "read-only", args: [], outputs: { response: { ok: "uint128", error: "uint128" } } },
|
|
29005
|
-
{ name: "get-symbol", access: "read-only", args: [], outputs: { response: { ok: { "string-ascii": { length: 32 } }, error: "uint128" } } },
|
|
29006
|
-
],
|
|
29007
|
-
} as const;
|
|
29008
|
-
|
|
29009
|
-
/** Tokens already labelled in this process — metadata is immutable, so one
|
|
29010
|
-
* resolution per token is enough even across a backfill. */
|
|
29011
|
-
const labelled = new Set<string>();
|
|
29012
|
-
|
|
29013
|
-
// Hoisted so the helper below can be typed against it.
|
|
29014
|
-
const schema = defineSchema({
|
|
29015
|
-
balances: {
|
|
29016
|
-
columns: {
|
|
29017
|
-
asset_identifier: { type: "text", indexed: true, search: true },
|
|
29018
|
-
holder: { type: "principal", indexed: true, search: true },
|
|
29019
|
-
amount: { type: "uint" },
|
|
29020
|
-
},
|
|
29021
|
-
// increment() upserts on this key.
|
|
29022
|
-
uniqueKeys: [["asset_identifier", "holder"]],
|
|
29023
|
-
},
|
|
29024
|
-
tokens: {
|
|
29025
|
-
columns: {
|
|
29026
|
-
asset_identifier: { type: "text", indexed: true },
|
|
29027
|
-
contract_id: { type: "text" },
|
|
29028
|
-
symbol: { type: "text" },
|
|
29029
|
-
decimals: { type: "uint" },
|
|
29030
|
-
},
|
|
29031
|
-
uniqueKeys: [["asset_identifier"]],
|
|
29032
|
-
},
|
|
29033
|
-
});
|
|
29034
|
-
|
|
29035
|
-
export default defineSubgraph({
|
|
29036
|
-
name: "${name}",
|
|
29037
|
-
version: "1.0.0",
|
|
29038
|
-
description: "Per-token balance tracking for any SIP-010 asset",
|
|
29039
|
-
|
|
29040
|
-
sources: {
|
|
29041
|
-
transfer: { type: "ft_transfer" },
|
|
29042
|
-
mint: { type: "ft_mint" },
|
|
29043
|
-
burn: { type: "ft_burn" },
|
|
29044
|
-
},
|
|
29045
|
-
|
|
29046
|
-
schema,
|
|
29047
|
-
|
|
29048
|
-
handlers: {
|
|
29049
|
-
transfer: async (event, ctx) => {
|
|
29050
|
-
await labelToken(event.assetIdentifier, ctx);
|
|
29051
|
-
const amount = BigInt(event.amount ?? 0);
|
|
29052
|
-
if (event.sender) {
|
|
29053
|
-
ctx.increment("balances", { asset_identifier: event.assetIdentifier, holder: event.sender }, { amount: -amount });
|
|
29054
|
-
}
|
|
29055
|
-
if (event.recipient) {
|
|
29056
|
-
ctx.increment("balances", { asset_identifier: event.assetIdentifier, holder: event.recipient }, { amount });
|
|
29057
|
-
}
|
|
29058
|
-
},
|
|
29059
|
-
mint: (event, ctx) => {
|
|
29060
|
-
if (event.recipient) {
|
|
29061
|
-
ctx.increment("balances", { asset_identifier: event.assetIdentifier, holder: event.recipient }, { amount: BigInt(event.amount ?? 0) });
|
|
29062
|
-
}
|
|
29063
|
-
},
|
|
29064
|
-
burn: (event, ctx) => {
|
|
29065
|
-
if (event.sender) {
|
|
29066
|
-
ctx.increment("balances", { asset_identifier: event.assetIdentifier, holder: event.sender }, { amount: -BigInt(event.amount ?? 0) });
|
|
29067
|
-
}
|
|
29068
|
-
},
|
|
29069
|
-
},
|
|
29070
|
-
});
|
|
29071
|
-
|
|
29072
|
-
/** Resolve a token's symbol + decimals from its contract, once. */
|
|
29073
|
-
async function labelToken(
|
|
29074
|
-
assetIdentifier: string | undefined,
|
|
29075
|
-
ctx: TypedSubgraphContext<typeof schema>,
|
|
29076
|
-
) {
|
|
29077
|
-
if (!assetIdentifier || labelled.has(assetIdentifier)) return;
|
|
29078
|
-
labelled.add(assetIdentifier);
|
|
29079
|
-
const contractId = assetIdentifier.split("::")[0];
|
|
29080
|
-
const token = readContractAt(ctx, contractId, SIP010_ABI, { cache: "contract-constant" });
|
|
29081
|
-
const [decimals, symbol] = await Promise.all([
|
|
29082
|
-
token.read.getDecimals({}),
|
|
29083
|
-
token.read.getSymbol({}),
|
|
29084
|
-
]);
|
|
29085
|
-
ctx.upsert("tokens", { asset_identifier: assetIdentifier }, { asset_identifier: assetIdentifier, contract_id: contractId, symbol, decimals });
|
|
29086
|
-
}
|
|
29087
|
-
`;
|
|
29088
|
-
}
|
|
29089
|
-
function sbtcFlows(name) {
|
|
29090
|
-
return `import { defineSubgraph } from "@secondlayer/subgraphs";
|
|
29091
|
-
|
|
29092
|
-
/**
|
|
29093
|
-
* Track sBTC protocol flows: deposits, withdrawals, signer rotations,
|
|
29094
|
-
* governance updates — your own indexed view of the sbtc-registry contract.
|
|
29095
|
-
*
|
|
29096
|
-
* Source contract: sbtc-registry (mainnet).
|
|
29097
|
-
*
|
|
29098
|
-
* Query examples once deployed:
|
|
29099
|
-
* GET /v1/subgraphs/${name}/flows?topic=completed-deposit
|
|
29100
|
-
* GET /v1/subgraphs/${name}/flows?topic=withdrawal-create
|
|
29101
|
-
*/
|
|
29102
|
-
export default defineSubgraph({
|
|
29103
|
-
name: "${name}",
|
|
29104
|
-
version: "1.0.0",
|
|
29105
|
-
description: "sBTC deposits, withdrawals, signer rotations, governance",
|
|
29106
|
-
|
|
29107
|
-
// Skip pre-sBTC history. Raise this (e.g., to a recent block near tip) for
|
|
29108
|
-
// a smaller backfill, or lower it if you need every sBTC event from genesis.
|
|
29109
|
-
startBlock: 860000,
|
|
29110
|
-
|
|
29111
|
-
sources: {
|
|
29112
|
-
registry: {
|
|
29113
|
-
type: "print_event",
|
|
29114
|
-
contractId: "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-registry",
|
|
29115
|
-
},
|
|
29116
|
-
},
|
|
29117
|
-
|
|
29118
|
-
schema: {
|
|
29119
|
-
flows: {
|
|
29120
|
-
columns: {
|
|
29121
|
-
topic: { type: "text", indexed: true, search: true },
|
|
29122
|
-
request_id: { type: "uint", nullable: true, indexed: true },
|
|
29123
|
-
amount: { type: "text", nullable: true },
|
|
29124
|
-
sender: { type: "principal", nullable: true, indexed: true },
|
|
29125
|
-
bitcoin_txid: { type: "text", nullable: true, search: true },
|
|
29126
|
-
burn_height: { type: "uint", nullable: true },
|
|
29127
|
-
},
|
|
29128
|
-
},
|
|
29129
|
-
},
|
|
29130
|
-
|
|
29131
|
-
handlers: {
|
|
29132
|
-
registry: (event, ctx) => {
|
|
29133
|
-
if (!event.topic) return;
|
|
29134
|
-
// event.data is untyped across topics — cast to the fields you read, or
|
|
29135
|
-
// declare \`prints\` per topic on the source for fully typed event.data.
|
|
29136
|
-
const data = event.data as {
|
|
29137
|
-
requestId?: bigint;
|
|
29138
|
-
amount?: bigint | string;
|
|
29139
|
-
sender?: string;
|
|
29140
|
-
bitcoinTxid?: string;
|
|
29141
|
-
burnHeight?: bigint;
|
|
29142
|
-
};
|
|
29143
|
-
ctx.insert("flows", {
|
|
29144
|
-
topic: event.topic,
|
|
29145
|
-
request_id: data.requestId ?? null,
|
|
29146
|
-
amount: data.amount != null ? String(data.amount) : null,
|
|
29147
|
-
sender: data.sender ?? null,
|
|
29148
|
-
bitcoin_txid: data.bitcoinTxid ?? null,
|
|
29149
|
-
burn_height: data.burnHeight ?? null,
|
|
29150
|
-
});
|
|
29151
|
-
},
|
|
29152
|
-
},
|
|
29153
|
-
});
|
|
29154
|
-
`;
|
|
29155
|
-
}
|
|
29156
|
-
function poxStacking(name) {
|
|
29157
|
-
return `import { defineSubgraph } from "@secondlayer/subgraphs";
|
|
29158
|
-
import { POX5_ABI } from "@secondlayer/stacks/pox5";
|
|
29159
|
-
|
|
29160
|
-
/**
|
|
29161
|
-
* Track PoX-5 \`stake\` calls with their arguments already decoded.
|
|
29162
|
-
*
|
|
29163
|
-
* pox-5 replaced pox-4 when Epoch 4.0 activated (Bitcoin block 960,230,
|
|
29164
|
-
* 2026-07-30). SIP-045 reshaped the API: stacking became bonds + staking, so
|
|
29165
|
-
* the entry point is \`stake\`, not \`stack-stx\`.
|
|
29166
|
-
*
|
|
29167
|
-
* The source pairs \`abi\` with \`functionName\`, which is what makes
|
|
29168
|
-
* \`event.input\` a typed, named argument set instead of positional
|
|
29169
|
-
* \`args[0] as bigint\`. Add one source per function you want to track.
|
|
29170
|
-
*
|
|
29171
|
-
* Query examples once deployed:
|
|
29172
|
-
* GET /v1/subgraphs/${name}/calls?caller=SP1...
|
|
29173
|
-
* GET /v1/subgraphs/${name}/calls?caller=SP1...
|
|
29174
|
-
*/
|
|
29175
|
-
export default defineSubgraph({
|
|
29176
|
-
name: "${name}",
|
|
29177
|
-
version: "1.0.0",
|
|
29178
|
-
description: "PoX-5 stake calls with decoded arguments",
|
|
29179
|
-
|
|
29180
|
-
sources: {
|
|
29181
|
-
pox: {
|
|
29182
|
-
type: "contract_call",
|
|
29183
|
-
// pox-5 is the live PoX contract since Epoch 4.0 activated at Bitcoin
|
|
29184
|
-
// block 960,230 (2026-07-30). Stacking history before that is pox-4 —
|
|
29185
|
-
// point a second source at \`.pox-4\` if you need it.
|
|
29186
|
-
contractId: "SP000000000000000000002Q6VF78.pox-5",
|
|
29187
|
-
// \`abi\` + \`functionName\` together are what type \`event.input\`: the
|
|
29188
|
-
// arguments cannot be typed without knowing which function was called.
|
|
29189
|
-
// POX5_ABI ships \`as const\` in @secondlayer/stacks, so there is no
|
|
29190
|
-
// hand-copied ABI to drift. Add a source per function you care about.
|
|
29191
|
-
functionName: "stake",
|
|
29192
|
-
abi: POX5_ABI,
|
|
29193
|
-
},
|
|
29194
|
-
},
|
|
29195
|
-
|
|
29196
|
-
schema: {
|
|
29197
|
-
calls: {
|
|
29198
|
-
columns: {
|
|
29199
|
-
function_name: { type: "text", indexed: true, search: true },
|
|
29200
|
-
caller: { type: "principal", indexed: true, search: true },
|
|
29201
|
-
amount_ustx: { type: "uint" },
|
|
29202
|
-
num_cycles: { type: "uint" },
|
|
29203
|
-
result_ok: { type: "boolean" },
|
|
29204
|
-
},
|
|
29205
|
-
},
|
|
29206
|
-
},
|
|
29207
|
-
|
|
29208
|
-
handlers: {
|
|
29209
|
-
pox: (event, ctx) => {
|
|
29210
|
-
// \`event.input\` is the decoded, named, typed argument set — no
|
|
29211
|
-
// \`args[0] as bigint\`. Hover \`amountUstx\` and it is a bigint.
|
|
29212
|
-
const resultHex = event.resultHex ?? "";
|
|
29213
|
-
ctx.insert("calls", {
|
|
29214
|
-
function_name: event.functionName || ctx.tx.functionName || "",
|
|
29215
|
-
caller: ctx.tx.sender,
|
|
29216
|
-
amount_ustx: event.input.amountUstx,
|
|
29217
|
-
num_cycles: event.input.numCycles,
|
|
29218
|
-
result_ok: resultHex.startsWith("0x07"), // 0x07 = response-ok type tag
|
|
29219
|
-
});
|
|
29220
|
-
},
|
|
29221
|
-
},
|
|
29222
|
-
});
|
|
29223
|
-
`;
|
|
29224
|
-
}
|
|
29225
|
-
function bnsNames(name) {
|
|
29226
|
-
return `import { defineSubgraph } from "@secondlayer/subgraphs";
|
|
29227
|
-
|
|
29228
|
-
/**
|
|
29229
|
-
* Track BNS-V2 name lifecycle events — registrations, transfers,
|
|
29230
|
-
* renewals, burns, airdrops.
|
|
29231
|
-
*
|
|
29232
|
-
* Source: BNS-V2 print events (topic-discriminated payloads).
|
|
29233
|
-
*
|
|
29234
|
-
* Query examples once deployed:
|
|
29235
|
-
* GET /v1/subgraphs/${name}/names?owner=SP1...
|
|
29236
|
-
* GET /v1/subgraphs/${name}/names?_search=alice
|
|
29237
|
-
*/
|
|
29238
|
-
export default defineSubgraph({
|
|
29239
|
-
name: "${name}",
|
|
29240
|
-
version: "1.0.0",
|
|
29241
|
-
description: "BNS-V2 name ownership and lifecycle",
|
|
29242
|
-
|
|
29243
|
-
sources: {
|
|
29244
|
-
bns: {
|
|
29245
|
-
type: "print_event",
|
|
29246
|
-
contractId: "SP2QEZ06AGJ3RKJPBV14SY1V5BBFNAW33D96YPGZF.BNS-V2",
|
|
29247
|
-
},
|
|
29248
|
-
},
|
|
29249
|
-
|
|
29250
|
-
schema: {
|
|
29251
|
-
names: {
|
|
29252
|
-
columns: {
|
|
29253
|
-
topic: { type: "text", indexed: true },
|
|
29254
|
-
namespace: { type: "text", indexed: true, search: true },
|
|
29255
|
-
name: { type: "text", indexed: true, search: true },
|
|
29256
|
-
fqn: { type: "text", indexed: true, search: true },
|
|
29257
|
-
owner: { type: "principal", nullable: true, indexed: true, search: true },
|
|
29258
|
-
},
|
|
29259
|
-
},
|
|
29260
|
-
},
|
|
29261
|
-
|
|
29262
|
-
handlers: {
|
|
29263
|
-
bns: (event, ctx) => {
|
|
29264
|
-
if (!event.topic) return;
|
|
29265
|
-
const data = event.data as { namespace?: unknown; name?: unknown; owner?: string };
|
|
29266
|
-
const namespace = decodeBuffUtf8(data.namespace);
|
|
29267
|
-
const nameLabel = decodeBuffUtf8(data.name);
|
|
29268
|
-
if (!namespace || !nameLabel) return;
|
|
29269
|
-
ctx.insert("names", {
|
|
29270
|
-
topic: event.topic,
|
|
29271
|
-
namespace,
|
|
29272
|
-
name: nameLabel,
|
|
29273
|
-
fqn: \`\${nameLabel}.\${namespace}\`,
|
|
29274
|
-
owner: event.topic === "burn-name" ? null : (data.owner ?? null),
|
|
29275
|
-
});
|
|
29276
|
-
},
|
|
29277
|
-
},
|
|
29278
|
-
});
|
|
29279
|
-
|
|
29280
|
-
function decodeBuffUtf8(value: unknown): string | null {
|
|
29281
|
-
if (typeof value !== "string") return null;
|
|
29282
|
-
const hex = value.startsWith("0x") ? value.slice(2) : value;
|
|
29283
|
-
if (hex.length === 0) return null;
|
|
29284
|
-
const bytes = new Uint8Array(hex.length / 2);
|
|
29285
|
-
for (let i = 0; i < bytes.length; i++) {
|
|
29286
|
-
bytes[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
29287
|
-
}
|
|
29288
|
-
let end = bytes.length;
|
|
29289
|
-
while (end > 0 && bytes[end - 1] === 0) end -= 1;
|
|
29290
|
-
return new TextDecoder("utf-8").decode(bytes.subarray(0, end));
|
|
29291
|
-
}
|
|
29292
|
-
`;
|
|
29293
|
-
}
|
|
29294
|
-
var SUBGRAPH_TEMPLATE_SLUGS, SUBGRAPH_TEMPLATE_DESCRIPTIONS;
|
|
29295
|
-
var init_subgraph = __esm(() => {
|
|
29296
|
-
SUBGRAPH_TEMPLATE_SLUGS = [
|
|
29297
|
-
"basic",
|
|
29298
|
-
"sip-010-balances",
|
|
29299
|
-
"sbtc-flows",
|
|
29300
|
-
"pox-stacking",
|
|
29301
|
-
"bns-names"
|
|
29302
|
-
];
|
|
29303
|
-
SUBGRAPH_TEMPLATE_DESCRIPTIONS = {
|
|
29304
|
-
basic: "Empty starter — pick any source filter type",
|
|
29305
|
-
"sip-010-balances": "SIP-010 token balances (transfers + mints + burns)",
|
|
29306
|
-
"sbtc-flows": "sBTC protocol flows (deposits, withdrawals, signer rotations)",
|
|
29307
|
-
"pox-stacking": "PoX-4 stacking lifecycle calls",
|
|
29308
|
-
"bns-names": "BNS-V2 name ownership and lifecycle"
|
|
29309
|
-
};
|
|
29310
|
-
});
|
|
29311
28956
|
|
|
29312
28957
|
// src/commands/subgraph-test.ts
|
|
29313
28958
|
var exports_subgraph_test = {};
|
|
@@ -29969,27 +29614,15 @@ function formatSubgraphSync(sync) {
|
|
|
29969
29614
|
}
|
|
29970
29615
|
function registerSubgraphsCommand(program2) {
|
|
29971
29616
|
const subgraphs = program2.command("subgraphs").description("Manage materialized subgraphs");
|
|
29972
|
-
subgraphs.command("create <name>").description("Create a new subgraph definition file").option("--
|
|
29617
|
+
subgraphs.command("create <name>").description("Create a new subgraph definition file").option("--from-contract <contractId>", "Generate sources/schema/handlers from the contract's observed print events (requires network)").option("--table-per-topic", "With --from-contract: one table per print topic instead of a single wide table").addHelpText("after", `
|
|
29973
29618
|
Examples:
|
|
29974
29619
|
$ secondlayer subgraphs create my-graph
|
|
29975
|
-
$ secondlayer subgraphs create token-balances --template sip-010-balances
|
|
29976
29620
|
$ secondlayer subgraphs create sbtc-flows --from-contract SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-registry
|
|
29977
29621
|
$ secondlayer subgraphs create sbtc-flows --from-contract SM3....sbtc-registry --table-per-topic`).action(async (name, opts) => {
|
|
29978
|
-
if (opts.fromContract && opts.template) {
|
|
29979
|
-
error("--from-contract and --template are mutually exclusive");
|
|
29980
|
-
process.exit(1);
|
|
29981
|
-
}
|
|
29982
29622
|
if (opts.tablePerTopic && !opts.fromContract) {
|
|
29983
29623
|
error("--table-per-topic requires --from-contract");
|
|
29984
29624
|
process.exit(1);
|
|
29985
29625
|
}
|
|
29986
|
-
const slug = opts.template ?? "basic";
|
|
29987
|
-
if (!opts.fromContract && !SUBGRAPH_TEMPLATE_SLUGS.includes(slug)) {
|
|
29988
|
-
error(`Unknown template "${opts.template}". Available templates:
|
|
29989
|
-
${SUBGRAPH_TEMPLATE_SLUGS.map((s2) => ` ${s2.padEnd(20)} ${SUBGRAPH_TEMPLATE_DESCRIPTIONS[s2]}`).join(`
|
|
29990
|
-
`)}`);
|
|
29991
|
-
process.exit(1);
|
|
29992
|
-
}
|
|
29993
29626
|
const dir = resolve3("subgraphs");
|
|
29994
29627
|
const filePath = resolve3(dir, `${name}.ts`);
|
|
29995
29628
|
if (existsSync3(filePath)) {
|
|
@@ -30018,7 +29651,7 @@ Check the contract id, and that the contract has emitted print events (schemas a
|
|
|
30018
29651
|
sample: schema.sample
|
|
30019
29652
|
}));
|
|
30020
29653
|
} else {
|
|
30021
|
-
content =
|
|
29654
|
+
content = generateSubgraphStarter(name);
|
|
30022
29655
|
}
|
|
30023
29656
|
if (!existsSync3(dir)) {
|
|
30024
29657
|
mkdirSync2(dir, { recursive: true });
|
|
@@ -30027,8 +29660,6 @@ Check the contract id, and that the contract has emitted print events (schemas a
|
|
|
30027
29660
|
success(`Created ${filePath}`);
|
|
30028
29661
|
if (opts.fromContract) {
|
|
30029
29662
|
info(`Schema inferred from ${opts.fromContract} print events (${opts.tablePerTopic ? "table per topic" : "single wide table"})`);
|
|
30030
|
-
} else if (slug !== "basic") {
|
|
30031
|
-
info(`Template: ${slug} — ${SUBGRAPH_TEMPLATE_DESCRIPTIONS[slug]}`);
|
|
30032
29663
|
}
|
|
30033
29664
|
info(`Next: secondlayer subgraphs deploy subgraphs/${name}.ts`);
|
|
30034
29665
|
});
|
|
@@ -30788,7 +30419,6 @@ var init_subgraphs2 = __esm(() => {
|
|
|
30788
30419
|
init_resolve_auth();
|
|
30789
30420
|
init_resolve_auth();
|
|
30790
30421
|
init_clarity();
|
|
30791
|
-
init_subgraph();
|
|
30792
30422
|
init_api();
|
|
30793
30423
|
init_format();
|
|
30794
30424
|
ORM_ONLY_CODEGEN_FLAGS = {
|
|
@@ -34690,14 +34320,14 @@ init_esm();
|
|
|
34690
34320
|
// package.json
|
|
34691
34321
|
var package_default = {
|
|
34692
34322
|
name: "@secondlayer/cli",
|
|
34693
|
-
version: "
|
|
34323
|
+
version: "12.1.0",
|
|
34324
|
+
description: "Run a Stacks index on your own hardware — set it up, restore verified history, and keep it current.",
|
|
34694
34325
|
repository: {
|
|
34695
34326
|
type: "git",
|
|
34696
34327
|
url: "git+https://github.com/ryanwaits/secondlayer.git",
|
|
34697
34328
|
directory: "packages/cli"
|
|
34698
34329
|
},
|
|
34699
34330
|
homepage: "https://github.com/ryanwaits/secondlayer/tree/main/packages/cli#readme",
|
|
34700
|
-
description: "CLI for subgraphs and blockchain indexing on Stacks",
|
|
34701
34331
|
type: "module",
|
|
34702
34332
|
bin: {
|
|
34703
34333
|
secondlayer: "./dist/cli.js",
|
|
@@ -34732,11 +34362,11 @@ var package_default = {
|
|
|
34732
34362
|
"@dsnp/parquetjs": "^1.8.8",
|
|
34733
34363
|
"@inquirer/prompts": "^8.2.0",
|
|
34734
34364
|
"@secondlayer/bundler": "^0.5.1",
|
|
34735
|
-
"@secondlayer/scaffold": "^1.5.
|
|
34736
|
-
"@secondlayer/sdk": "^
|
|
34737
|
-
"@secondlayer/shared": "^
|
|
34738
|
-
"@secondlayer/stacks": "^
|
|
34739
|
-
"@secondlayer/subgraphs": "^4.0.
|
|
34365
|
+
"@secondlayer/scaffold": "^1.5.4",
|
|
34366
|
+
"@secondlayer/sdk": "^8.0.1",
|
|
34367
|
+
"@secondlayer/shared": "^10.0.0",
|
|
34368
|
+
"@secondlayer/stacks": "^4.0.0",
|
|
34369
|
+
"@secondlayer/subgraphs": "^4.0.2",
|
|
34740
34370
|
esbuild: "^0.27.4",
|
|
34741
34371
|
"smol-toml": "^1.6.1",
|
|
34742
34372
|
zod: "^4.3.6"
|
|
@@ -39063,6 +38693,7 @@ Exit codes:
|
|
|
39063
38693
|
});
|
|
39064
38694
|
}
|
|
39065
38695
|
// src/commands/bootstrap.ts
|
|
38696
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
39066
38697
|
import { unlink as unlink3, writeFile as writeFile6 } from "node:fs/promises";
|
|
39067
38698
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
39068
38699
|
import { join as join16 } from "node:path";
|
|
@@ -39074,6 +38705,7 @@ import {
|
|
|
39074
38705
|
} from "@secondlayer/shared/archive/copy-loader";
|
|
39075
38706
|
import { computeRangeDigest as computeRangeDigest3 } from "@secondlayer/shared/archive/range-digest";
|
|
39076
38707
|
import { getDb as getDb4, getRawClient, sql as sql3 } from "@secondlayer/shared/db";
|
|
38708
|
+
import { upsertSyncScope } from "@secondlayer/shared/db/queries/sync-scope";
|
|
39077
38709
|
|
|
39078
38710
|
// src/lib/bootstrap-resume.ts
|
|
39079
38711
|
function planTornImport(input4) {
|
|
@@ -39110,6 +38742,18 @@ var BOOTSTRAP_EXIT = {
|
|
|
39110
38742
|
};
|
|
39111
38743
|
var BLOCKS_PER_DAY_ESTIMATE = 5400;
|
|
39112
38744
|
var RESTORE_ROWS_PER_SECOND = 30000;
|
|
38745
|
+
function archiveScopeBounds(partitions) {
|
|
38746
|
+
if (partitions.length === 0)
|
|
38747
|
+
return null;
|
|
38748
|
+
return {
|
|
38749
|
+
start_height: Math.min(...partitions.map((p2) => p2.from_block)),
|
|
38750
|
+
tip_height: Math.max(...partitions.map((p2) => p2.to_block))
|
|
38751
|
+
};
|
|
38752
|
+
}
|
|
38753
|
+
function manifestDigest(manifest) {
|
|
38754
|
+
const { signature: _signature, key_id: _keyId, ...payload } = manifest;
|
|
38755
|
+
return createHash3("sha256").update(JSON.stringify(payload)).digest("hex");
|
|
38756
|
+
}
|
|
39113
38757
|
async function readNodeTip() {
|
|
39114
38758
|
const url = process.env.STACKS_NODE_RPC_URL ?? "http://localhost:20443";
|
|
39115
38759
|
try {
|
|
@@ -39164,10 +38808,11 @@ async function loadPartition(reference, partition, rawClient) {
|
|
|
39164
38808
|
return written;
|
|
39165
38809
|
}
|
|
39166
38810
|
function attachBootstrapCommand(cmd) {
|
|
39167
|
-
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", `
|
|
38811
|
+
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("--from-block <n>", "forward-only: restore from this height instead of genesis; earlier history is declared out of scope").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", `
|
|
39168
38812
|
Examples:
|
|
39169
38813
|
$ secondlayer bootstrap --against https://archive.secondlayer.tools/.../snapshots/<digest>.json
|
|
39170
38814
|
$ secondlayer bootstrap --against ./snapshot.json --to-block 4000000 --yes
|
|
38815
|
+
$ secondlayer bootstrap --against ./snapshot.json --from-block 8000000 --yes
|
|
39171
38816
|
|
|
39172
38817
|
Exit codes:
|
|
39173
38818
|
0 restored and verified
|
|
@@ -39192,7 +38837,8 @@ Exit codes:
|
|
|
39192
38837
|
const maxHeightRow = await db.selectFrom("blocks").select(({ fn }) => fn.max("height").as("max")).executeTakeFirst();
|
|
39193
38838
|
const maxBlockHeight = maxHeightRow?.max === null || maxHeightRow?.max === undefined ? null : Number(maxHeightRow.max);
|
|
39194
38839
|
const toBlock = opts.toBlock === undefined ? undefined : Number(opts.toBlock);
|
|
39195
|
-
const
|
|
38840
|
+
const fromBlock = opts.fromBlock === undefined ? undefined : Number(opts.fromBlock);
|
|
38841
|
+
const declared = (reference.manifest.partitions ?? []).filter((p2) => (toBlock === undefined || p2.to_block <= toBlock) && (fromBlock === undefined || p2.from_block >= fromBlock));
|
|
39196
38842
|
const resume = planTornImport({
|
|
39197
38843
|
hasIndexProgress: !!progress,
|
|
39198
38844
|
maxBlockHeight,
|
|
@@ -39219,6 +38865,8 @@ Exit codes:
|
|
|
39219
38865
|
});
|
|
39220
38866
|
process.exit(BOOTSTRAP_EXIT.REFUSED);
|
|
39221
38867
|
}
|
|
38868
|
+
const scopeBounds = archiveScopeBounds(declared);
|
|
38869
|
+
const startHeight = scopeBounds?.start_height ?? 0;
|
|
39222
38870
|
const totalRows = partitions.reduce((sum, p2) => sum + p2.row_count, 0);
|
|
39223
38871
|
const totalBytes = partitions.reduce((sum, p2) => sum + p2.byte_size, 0);
|
|
39224
38872
|
const tipHeight = Math.max(...partitions.map((p2) => p2.to_block));
|
|
@@ -39228,7 +38876,10 @@ Exit codes:
|
|
|
39228
38876
|
console.error("");
|
|
39229
38877
|
console.error(formatKeyValue([
|
|
39230
38878
|
["archive", reference.origin],
|
|
39231
|
-
[
|
|
38879
|
+
[
|
|
38880
|
+
"coverage",
|
|
38881
|
+
`${startHeight === 0 ? "genesis" : startHeight.toLocaleString()} → ${tipHeight.toLocaleString()}`
|
|
38882
|
+
],
|
|
39232
38883
|
["rows", totalRows.toLocaleString()],
|
|
39233
38884
|
["download", `${(totalBytes / 1e9).toFixed(1)} GB`],
|
|
39234
38885
|
["signature", "verified"]
|
|
@@ -39286,6 +38937,17 @@ Exit codes:
|
|
|
39286
38937
|
highest_seen_block = GREATEST(index_progress.highest_seen_block, EXCLUDED.highest_seen_block),
|
|
39287
38938
|
updated_at = NOW()
|
|
39288
38939
|
`.execute(db);
|
|
38940
|
+
const genesisRow = startHeight === 0 ? await db.selectFrom("blocks").select("hash").where("canonical", "=", true).where("height", "=", 0).executeTakeFirst() : undefined;
|
|
38941
|
+
await upsertSyncScope(db, {
|
|
38942
|
+
network,
|
|
38943
|
+
start_height: startHeight,
|
|
38944
|
+
target_height: null,
|
|
38945
|
+
bootstrap: {
|
|
38946
|
+
source: "archive",
|
|
38947
|
+
manifest_digest: manifestDigest(reference.manifest),
|
|
38948
|
+
genesis_hash: genesisRow?.hash ?? null
|
|
38949
|
+
}
|
|
38950
|
+
});
|
|
39289
38951
|
const seam = nodeTipAtStart !== null && nodeTipAtStart > tipHeight ? {
|
|
39290
38952
|
nodeTip: nodeTipAtStart,
|
|
39291
38953
|
gap: nodeTipAtStart - tipHeight
|
|
@@ -39294,6 +38956,7 @@ Exit codes:
|
|
|
39294
38956
|
const report = {
|
|
39295
38957
|
status: divergent === 0 ? "restored" : "divergent",
|
|
39296
38958
|
archive: reference.origin,
|
|
38959
|
+
start_height: startHeight,
|
|
39297
38960
|
tip_height: tipHeight,
|
|
39298
38961
|
rows: loaded,
|
|
39299
38962
|
ranges_verified: referenceDigests.length,
|
|
@@ -39316,6 +38979,9 @@ Exit codes:
|
|
|
39316
38979
|
}
|
|
39317
38980
|
console.error("");
|
|
39318
38981
|
console.error(dim(`Your instance holds history through ${tipHeight.toLocaleString()} and will resume at ${(tipHeight + 1).toLocaleString()}.`));
|
|
38982
|
+
if (startHeight > 0) {
|
|
38983
|
+
console.error(dim(` Scope starts at ${startHeight.toLocaleString()} — earlier history is declared out of scope, not missing.`));
|
|
38984
|
+
}
|
|
39319
38985
|
if (seam) {
|
|
39320
38986
|
console.error(dim(`The chain advanced to ${seam.nodeTip.toLocaleString()} while restoring — ${seam.gap.toLocaleString()} blocks to catch up.`));
|
|
39321
38987
|
console.error(dim(" Start the indexer; it backfills that range before following the tip."));
|
|
@@ -39340,10 +39006,339 @@ Exit codes:
|
|
|
39340
39006
|
function registerBootstrapCommand(program2) {
|
|
39341
39007
|
attachBootstrapCommand(program2.command("bootstrap").description("Restore chain history from a verified archive instead of syncing from genesis"));
|
|
39342
39008
|
}
|
|
39009
|
+
// src/commands/uninstall.ts
|
|
39010
|
+
init_output();
|
|
39011
|
+
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
39012
|
+
import { existsSync as existsSync8, readFileSync as readFileSync7 } from "node:fs";
|
|
39013
|
+
import { join as join17, resolve as resolve13 } from "node:path";
|
|
39014
|
+
import { planUninstall, uninstallCommand } from "@secondlayer/shared/runtime";
|
|
39015
|
+
var UNINSTALL_EXIT = { OK: 0, FAILED: 1, REFUSED: 2 };
|
|
39016
|
+
var DEFAULT_COMPOSE = "docker/oss/docker-compose.yml";
|
|
39017
|
+
function bundleHasKeys(bundleDir) {
|
|
39018
|
+
const manifest = join17(bundleDir, "manifest.json");
|
|
39019
|
+
if (!existsSync8(manifest))
|
|
39020
|
+
return false;
|
|
39021
|
+
try {
|
|
39022
|
+
const parsed = JSON.parse(readFileSync7(manifest, "utf8"));
|
|
39023
|
+
return parsed.secrets != null;
|
|
39024
|
+
} catch {
|
|
39025
|
+
return false;
|
|
39026
|
+
}
|
|
39027
|
+
}
|
|
39028
|
+
function registerUninstallCommand(program2) {
|
|
39029
|
+
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) => {
|
|
39030
|
+
const dataDir = process.env.DATA_DIR ?? "./data";
|
|
39031
|
+
const secretsPresent = !!process.env.SECONDLAYER_SECRETS_KEY || existsSync8(resolve13(process.cwd(), ".env.local"));
|
|
39032
|
+
const keysBackedUp = opts.backup ? bundleHasKeys(resolve13(opts.backup)) : false;
|
|
39033
|
+
const decision = planUninstall({
|
|
39034
|
+
purge: opts.purge === true,
|
|
39035
|
+
confirmed: opts.yes === true,
|
|
39036
|
+
keysBackedUp,
|
|
39037
|
+
secretsPresent,
|
|
39038
|
+
dataDir
|
|
39039
|
+
});
|
|
39040
|
+
if (!decision.ok) {
|
|
39041
|
+
printError(decision.reason);
|
|
39042
|
+
process.exit(UNINSTALL_EXIT.REFUSED);
|
|
39043
|
+
}
|
|
39044
|
+
const cmd = uninstallCommand(decision.plan, opts.compose);
|
|
39045
|
+
if (!opts.apply) {
|
|
39046
|
+
output({
|
|
39047
|
+
json: opts.json,
|
|
39048
|
+
data: { dryRun: true, plan: decision.plan, command: cmd },
|
|
39049
|
+
human: () => {
|
|
39050
|
+
note("dry run — nothing removed. Pass --apply to proceed.");
|
|
39051
|
+
note(`would run: docker ${cmd.join(" ")}`);
|
|
39052
|
+
for (const p2 of decision.plan.preserves) {
|
|
39053
|
+
note(`preserved · ${p2.what}: ${p2.detail}`);
|
|
39054
|
+
}
|
|
39055
|
+
for (const d2 of decision.plan.destroys) {
|
|
39056
|
+
warn(`DESTROYED · volume ${d2}`);
|
|
39057
|
+
}
|
|
39058
|
+
}
|
|
39059
|
+
});
|
|
39060
|
+
return;
|
|
39061
|
+
}
|
|
39062
|
+
const res = spawnSync3("docker", cmd, { stdio: "inherit" });
|
|
39063
|
+
if ((res.status ?? 1) !== 0) {
|
|
39064
|
+
printError(`docker ${cmd.join(" ")} exited ${res.status}`);
|
|
39065
|
+
process.exit(UNINSTALL_EXIT.FAILED);
|
|
39066
|
+
}
|
|
39067
|
+
output({
|
|
39068
|
+
json: opts.json,
|
|
39069
|
+
data: { removed: true, plan: decision.plan },
|
|
39070
|
+
human: () => {
|
|
39071
|
+
success("stack removed");
|
|
39072
|
+
for (const w2 of decision.warnings)
|
|
39073
|
+
warn(w2);
|
|
39074
|
+
for (const p2 of decision.plan.preserves) {
|
|
39075
|
+
note(`preserved · ${p2.what}: ${p2.detail}`);
|
|
39076
|
+
}
|
|
39077
|
+
}
|
|
39078
|
+
});
|
|
39079
|
+
});
|
|
39080
|
+
}
|
|
39081
|
+
// src/commands/backup.ts
|
|
39082
|
+
init_output();
|
|
39083
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
39084
|
+
import {
|
|
39085
|
+
chmodSync,
|
|
39086
|
+
existsSync as existsSync9,
|
|
39087
|
+
mkdirSync as mkdirSync6,
|
|
39088
|
+
readFileSync as readFileSync8,
|
|
39089
|
+
statSync as statSync3,
|
|
39090
|
+
writeFileSync as writeFileSync7
|
|
39091
|
+
} from "node:fs";
|
|
39092
|
+
import { join as join18, resolve as resolve14 } from "node:path";
|
|
39093
|
+
import { closeDb, getDb as getDb5, sql as sql4 } from "@secondlayer/shared/db";
|
|
39094
|
+
import {
|
|
39095
|
+
decryptBundle,
|
|
39096
|
+
encryptBundle,
|
|
39097
|
+
keyMatchesCanary,
|
|
39098
|
+
planBackup,
|
|
39099
|
+
precheckRestore,
|
|
39100
|
+
sealKeyCanary
|
|
39101
|
+
} from "@secondlayer/shared/runtime";
|
|
39102
|
+
var BACKUP_EXIT = { OK: 0, FAILED: 1, REFUSED: 2 };
|
|
39103
|
+
var MANIFEST_FILE = "manifest.json";
|
|
39104
|
+
var DB_FILE = "db.dump";
|
|
39105
|
+
var SECRETS_FILE = "secrets.enc";
|
|
39106
|
+
var SECRET_ENV_KEYS = [
|
|
39107
|
+
"INSTANCE_TOKEN",
|
|
39108
|
+
"SECONDLAYER_SECRETS_KEY",
|
|
39109
|
+
"STREAMS_SIGNING_PRIVATE_KEY",
|
|
39110
|
+
"SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY",
|
|
39111
|
+
"SL_API_KEY",
|
|
39112
|
+
"ARCHIVE_SIGNING_PUBLIC_KEY"
|
|
39113
|
+
];
|
|
39114
|
+
function readSecretsKey() {
|
|
39115
|
+
const key = process.env.SECONDLAYER_SECRETS_KEY?.trim();
|
|
39116
|
+
return key ? key : undefined;
|
|
39117
|
+
}
|
|
39118
|
+
function sha256File(path11) {
|
|
39119
|
+
return createHash4("sha256").update(readFileSync8(path11)).digest("hex");
|
|
39120
|
+
}
|
|
39121
|
+
async function readScope() {
|
|
39122
|
+
const db = getDb5();
|
|
39123
|
+
const bounds = await sql4`SELECT MIN(height)::text AS min, MAX(height)::text AS max FROM blocks WHERE canonical = true`.execute(db);
|
|
39124
|
+
const row = bounds.rows[0];
|
|
39125
|
+
const network = process.env.STACKS_NETWORK ?? "mainnet";
|
|
39126
|
+
return {
|
|
39127
|
+
network,
|
|
39128
|
+
fromHeight: row?.min ? Number(row.min) : 0,
|
|
39129
|
+
toHeight: row?.max ? Number(row.max) : null
|
|
39130
|
+
};
|
|
39131
|
+
}
|
|
39132
|
+
function requireDatabaseUrl() {
|
|
39133
|
+
const url = process.env.DATABASE_URL?.trim();
|
|
39134
|
+
if (!url)
|
|
39135
|
+
throw new Error("DATABASE_URL is required");
|
|
39136
|
+
return url;
|
|
39137
|
+
}
|
|
39138
|
+
async function runProcess(cmd, label) {
|
|
39139
|
+
const proc = Bun.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
|
|
39140
|
+
const stderr = await new Response(proc.stderr).text();
|
|
39141
|
+
const code = await proc.exited;
|
|
39142
|
+
if (code !== 0) {
|
|
39143
|
+
return { ok: false, stderr: `${label} exited ${code}: ${stderr.trim()}` };
|
|
39144
|
+
}
|
|
39145
|
+
return { ok: true, stderr };
|
|
39146
|
+
}
|
|
39147
|
+
function attachBackupCommand(cmd) {
|
|
39148
|
+
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) => {
|
|
39149
|
+
try {
|
|
39150
|
+
await runBackup(opts);
|
|
39151
|
+
await closeDb();
|
|
39152
|
+
} catch (error2) {
|
|
39153
|
+
printError(error2 instanceof Error ? error2.message : String(error2));
|
|
39154
|
+
process.exit(BACKUP_EXIT.FAILED);
|
|
39155
|
+
}
|
|
39156
|
+
});
|
|
39157
|
+
}
|
|
39158
|
+
async function runBackup(opts) {
|
|
39159
|
+
const databaseUrl = requireDatabaseUrl();
|
|
39160
|
+
const includeSecrets = opts.secrets !== false;
|
|
39161
|
+
const passphrase = opts.passphrase ?? process.env.SECONDLAYER_BACKUP_PASSPHRASE ?? "";
|
|
39162
|
+
const secretsKey = readSecretsKey();
|
|
39163
|
+
if (includeSecrets && !secretsKey) {
|
|
39164
|
+
printError("SECONDLAYER_SECRETS_KEY is not set, so this backup cannot carry the keys that decrypt your data. Set it, or pass --no-secrets to take a data-only bundle.");
|
|
39165
|
+
process.exit(BACKUP_EXIT.REFUSED);
|
|
39166
|
+
}
|
|
39167
|
+
const scope = await readScope();
|
|
39168
|
+
const plan = planBackup({
|
|
39169
|
+
network: scope.network,
|
|
39170
|
+
fromHeight: scope.fromHeight,
|
|
39171
|
+
toHeight: scope.toHeight,
|
|
39172
|
+
includeSecrets,
|
|
39173
|
+
secretsEncrypted: includeSecrets && passphrase.length > 0,
|
|
39174
|
+
canary: secretsKey ? sealKeyCanary(secretsKey) : undefined
|
|
39175
|
+
});
|
|
39176
|
+
if (!plan.ok) {
|
|
39177
|
+
printError(plan.reason);
|
|
39178
|
+
process.exit(BACKUP_EXIT.REFUSED);
|
|
39179
|
+
}
|
|
39180
|
+
const outDir = resolve14(opts.out);
|
|
39181
|
+
mkdirSync6(outDir, { recursive: true });
|
|
39182
|
+
console.error("dumping database…");
|
|
39183
|
+
const dumpPath = join18(outDir, DB_FILE);
|
|
39184
|
+
const dump = await runProcess(["pg_dump", "-Fc", "--no-owner", "--no-acl", "-f", dumpPath, databaseUrl], "pg_dump");
|
|
39185
|
+
if (!dump.ok) {
|
|
39186
|
+
printError(dump.stderr);
|
|
39187
|
+
process.exit(BACKUP_EXIT.FAILED);
|
|
39188
|
+
}
|
|
39189
|
+
const manifest = {
|
|
39190
|
+
...plan.manifest,
|
|
39191
|
+
db: {
|
|
39192
|
+
file: DB_FILE,
|
|
39193
|
+
sha256: sha256File(dumpPath),
|
|
39194
|
+
bytes: statSync3(dumpPath).size,
|
|
39195
|
+
format: "pg_dump-custom"
|
|
39196
|
+
}
|
|
39197
|
+
};
|
|
39198
|
+
if (includeSecrets && secretsKey) {
|
|
39199
|
+
const lines2 = SECRET_ENV_KEYS.filter((k2) => process.env[k2]).map((k2) => `${k2}=${process.env[k2]}`);
|
|
39200
|
+
const sealed = encryptBundle(lines2.join(`
|
|
39201
|
+
`), passphrase);
|
|
39202
|
+
const secretsPath = join18(outDir, SECRETS_FILE);
|
|
39203
|
+
writeFileSync7(secretsPath, sealed);
|
|
39204
|
+
chmodSync(secretsPath, 384);
|
|
39205
|
+
manifest.secrets = {
|
|
39206
|
+
file: SECRETS_FILE,
|
|
39207
|
+
encrypted: true,
|
|
39208
|
+
canary: sealKeyCanary(secretsKey)
|
|
39209
|
+
};
|
|
39210
|
+
}
|
|
39211
|
+
writeFileSync7(join18(outDir, MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}
|
|
39212
|
+
`);
|
|
39213
|
+
output({
|
|
39214
|
+
json: opts.json,
|
|
39215
|
+
data: { out: outDir, manifest },
|
|
39216
|
+
human: () => {
|
|
39217
|
+
success(`backup written to ${outDir}`);
|
|
39218
|
+
note(`blocks ${manifest.scope.from_height}–${manifest.scope.to_height ?? "?"} · ${(manifest.db?.bytes ?? 0) / 1024 ** 2 | 0} MB`);
|
|
39219
|
+
if (!manifest.secrets) {
|
|
39220
|
+
warn("no keys in this bundle — it can restore data, but not a working instance");
|
|
39221
|
+
} else {
|
|
39222
|
+
note("keys are encrypted; keep the passphrase somewhere else");
|
|
39223
|
+
}
|
|
39224
|
+
}
|
|
39225
|
+
});
|
|
39226
|
+
}
|
|
39227
|
+
function attachRestoreCommand(cmd) {
|
|
39228
|
+
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) => {
|
|
39229
|
+
try {
|
|
39230
|
+
await runRestore(opts);
|
|
39231
|
+
await closeDb();
|
|
39232
|
+
} catch (error2) {
|
|
39233
|
+
printError(error2 instanceof Error ? error2.message : String(error2));
|
|
39234
|
+
process.exit(BACKUP_EXIT.FAILED);
|
|
39235
|
+
}
|
|
39236
|
+
});
|
|
39237
|
+
}
|
|
39238
|
+
async function runRestore(opts) {
|
|
39239
|
+
const databaseUrl = requireDatabaseUrl();
|
|
39240
|
+
const dir = resolve14(opts.from);
|
|
39241
|
+
const manifestPath = join18(dir, MANIFEST_FILE);
|
|
39242
|
+
if (!existsSync9(manifestPath)) {
|
|
39243
|
+
printError(`no ${MANIFEST_FILE} in ${dir}`);
|
|
39244
|
+
process.exit(BACKUP_EXIT.REFUSED);
|
|
39245
|
+
}
|
|
39246
|
+
const manifest = JSON.parse(readFileSync8(manifestPath, "utf8"));
|
|
39247
|
+
const dumpPath = join18(dir, manifest.db?.file ?? DB_FILE);
|
|
39248
|
+
if (manifest.db && sha256File(dumpPath) !== manifest.db.sha256) {
|
|
39249
|
+
printError("the database dump does not match the digest in the manifest; this bundle is corrupt or truncated");
|
|
39250
|
+
process.exit(BACKUP_EXIT.REFUSED);
|
|
39251
|
+
}
|
|
39252
|
+
const envKey = readSecretsKey();
|
|
39253
|
+
let bundleKey;
|
|
39254
|
+
if (manifest.secrets) {
|
|
39255
|
+
const passphrase = opts.passphrase ?? process.env.SECONDLAYER_BACKUP_PASSPHRASE ?? "";
|
|
39256
|
+
if (!passphrase) {
|
|
39257
|
+
printError("this bundle carries encrypted keys; supply --passphrase to read them");
|
|
39258
|
+
process.exit(BACKUP_EXIT.REFUSED);
|
|
39259
|
+
}
|
|
39260
|
+
try {
|
|
39261
|
+
const env2 = decryptBundle(readFileSync8(join18(dir, manifest.secrets.file)), passphrase);
|
|
39262
|
+
bundleKey = env2.split(`
|
|
39263
|
+
`).find((l2) => l2.startsWith("SECONDLAYER_SECRETS_KEY="))?.slice("SECONDLAYER_SECRETS_KEY=".length).trim();
|
|
39264
|
+
} catch {
|
|
39265
|
+
printError("could not decrypt the secrets bundle — wrong passphrase?");
|
|
39266
|
+
process.exit(BACKUP_EXIT.REFUSED);
|
|
39267
|
+
}
|
|
39268
|
+
}
|
|
39269
|
+
if (envKey && manifest.secrets && !keyMatchesCanary(manifest.secrets.canary, envKey)) {
|
|
39270
|
+
printError("SECONDLAYER_SECRETS_KEY in this environment is NOT the key this backup was taken with. Restoring would leave every encrypted column unreadable. Unset it to install the bundle's key, or point this restore at the matching instance.");
|
|
39271
|
+
process.exit(BACKUP_EXIT.REFUSED);
|
|
39272
|
+
}
|
|
39273
|
+
const db = getDb5();
|
|
39274
|
+
const present = await sql4`SELECT to_regclass('public.blocks') IS NOT NULL AS present`.execute(db);
|
|
39275
|
+
let targetIsEmpty = true;
|
|
39276
|
+
if (present.rows[0]?.present) {
|
|
39277
|
+
const existing = await sql4`SELECT COUNT(*)::text AS count FROM blocks`.execute(db);
|
|
39278
|
+
targetIsEmpty = (existing.rows[0]?.count ?? "0") === "0";
|
|
39279
|
+
}
|
|
39280
|
+
const check = precheckRestore({
|
|
39281
|
+
manifest,
|
|
39282
|
+
targetNetwork: process.env.STACKS_NETWORK ?? "mainnet",
|
|
39283
|
+
targetIsEmpty,
|
|
39284
|
+
force: opts.force === true,
|
|
39285
|
+
secretsKeyHex: envKey ?? bundleKey
|
|
39286
|
+
});
|
|
39287
|
+
if (!check.ok) {
|
|
39288
|
+
printError(check.reason);
|
|
39289
|
+
process.exit(BACKUP_EXIT.REFUSED);
|
|
39290
|
+
}
|
|
39291
|
+
if (!opts.apply) {
|
|
39292
|
+
output({
|
|
39293
|
+
json: opts.json,
|
|
39294
|
+
data: { dryRun: true, manifest, targetIsEmpty },
|
|
39295
|
+
human: () => {
|
|
39296
|
+
note("dry run — nothing written. Pass --apply to restore.");
|
|
39297
|
+
note(`would restore ${manifest.network} blocks ${manifest.scope.from_height}–${manifest.scope.to_height ?? "?"}`);
|
|
39298
|
+
if (bundleKey && !envKey) {
|
|
39299
|
+
note("after restoring, install the bundle's keys before starting the runtime");
|
|
39300
|
+
}
|
|
39301
|
+
}
|
|
39302
|
+
});
|
|
39303
|
+
return;
|
|
39304
|
+
}
|
|
39305
|
+
console.error("restoring database…");
|
|
39306
|
+
const restore = await runProcess([
|
|
39307
|
+
"pg_restore",
|
|
39308
|
+
"--no-owner",
|
|
39309
|
+
"--no-acl",
|
|
39310
|
+
"--clean",
|
|
39311
|
+
"--if-exists",
|
|
39312
|
+
"-d",
|
|
39313
|
+
databaseUrl,
|
|
39314
|
+
dumpPath
|
|
39315
|
+
], "pg_restore");
|
|
39316
|
+
if (!restore.ok) {
|
|
39317
|
+
printError(restore.stderr);
|
|
39318
|
+
process.exit(BACKUP_EXIT.FAILED);
|
|
39319
|
+
}
|
|
39320
|
+
output({
|
|
39321
|
+
json: opts.json,
|
|
39322
|
+
data: { restored: true, manifest },
|
|
39323
|
+
human: () => {
|
|
39324
|
+
success("restore complete");
|
|
39325
|
+
if (bundleKey && !envKey) {
|
|
39326
|
+
warn("set SECONDLAYER_SECRETS_KEY from the bundle before starting the runtime, or it will generate a new one and the restored data will not decrypt");
|
|
39327
|
+
}
|
|
39328
|
+
note("verify with: secondlayer verify all --against <manifest>");
|
|
39329
|
+
}
|
|
39330
|
+
});
|
|
39331
|
+
}
|
|
39332
|
+
function registerBackupCommand(program2) {
|
|
39333
|
+
attachBackupCommand(program2.command("backup").description("Write a restorable bundle: index, keys, and scope"));
|
|
39334
|
+
}
|
|
39335
|
+
function registerRestoreCommand(program2) {
|
|
39336
|
+
attachRestoreCommand(program2.command("restore").description("Restore an instance from a backup bundle"));
|
|
39337
|
+
}
|
|
39343
39338
|
// src/lib/instance-init.ts
|
|
39344
39339
|
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
39345
|
-
import { existsSync as
|
|
39346
|
-
import { resolve as
|
|
39340
|
+
import { existsSync as existsSync10, readFileSync as readFileSync9, writeFileSync as writeFileSync8 } from "node:fs";
|
|
39341
|
+
import { resolve as resolve15 } from "node:path";
|
|
39347
39342
|
import { generateEd25519KeyPair } from "@secondlayer/shared/crypto/ed25519";
|
|
39348
39343
|
import { INSTANCE_NETWORKS } from "@secondlayer/shared/db/queries/instance";
|
|
39349
39344
|
var INSTANCE_ENV_FILE = ".env.local";
|
|
@@ -39377,9 +39372,9 @@ function unescapeEnvValue(raw) {
|
|
|
39377
39372
|
`).replace(/\\"/g, '"').replace(/\\\\/g, "\\");
|
|
39378
39373
|
}
|
|
39379
39374
|
function readEnvValue(envPath, key) {
|
|
39380
|
-
if (!
|
|
39375
|
+
if (!existsSync10(envPath))
|
|
39381
39376
|
return null;
|
|
39382
|
-
const match =
|
|
39377
|
+
const match = readFileSync9(envPath, "utf8").match(new RegExp(`^${key}=(.*)$`, "m"));
|
|
39383
39378
|
return match ? unescapeEnvValue(match[1]) : null;
|
|
39384
39379
|
}
|
|
39385
39380
|
function buildInstanceEnv(input4) {
|
|
@@ -39422,12 +39417,12 @@ function renderInstanceEnv(env2) {
|
|
|
39422
39417
|
`);
|
|
39423
39418
|
}
|
|
39424
39419
|
function writeInstanceEnv(cwd, env2, file = INSTANCE_ENV_FILE) {
|
|
39425
|
-
const path11 =
|
|
39426
|
-
|
|
39420
|
+
const path11 = resolve15(cwd, file);
|
|
39421
|
+
writeFileSync8(path11, renderInstanceEnv(env2), { mode: 384 });
|
|
39427
39422
|
return path11;
|
|
39428
39423
|
}
|
|
39429
39424
|
function loadExistingInstanceEnv(cwd, file = INSTANCE_ENV_FILE) {
|
|
39430
|
-
const path11 =
|
|
39425
|
+
const path11 = resolve15(cwd, file);
|
|
39431
39426
|
return {
|
|
39432
39427
|
INSTANCE_TOKEN: readEnvValue(path11, "INSTANCE_TOKEN"),
|
|
39433
39428
|
SECONDLAYER_SECRETS_KEY: readEnvValue(path11, "SECONDLAYER_SECRETS_KEY"),
|
|
@@ -39581,7 +39576,7 @@ function registerStartCommand(program2) {
|
|
|
39581
39576
|
}
|
|
39582
39577
|
// src/cli.ts
|
|
39583
39578
|
var { version } = package_default;
|
|
39584
|
-
program.name("secondlayer").alias("sl").description("Secondlayer CLI —
|
|
39579
|
+
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)");
|
|
39585
39580
|
program.hook("preAction", (thisCommand) => {
|
|
39586
39581
|
const { network, apiKey, apiUrl } = thisCommand.opts();
|
|
39587
39582
|
if (network)
|
|
@@ -39595,7 +39590,7 @@ program.addHelpText("after", `
|
|
|
39595
39590
|
Quickstart:
|
|
39596
39591
|
$ secondlayer init --network mainnet
|
|
39597
39592
|
$ secondlayer bootstrap --against <manifest>
|
|
39598
|
-
$ secondlayer subgraphs create my-watcher --
|
|
39593
|
+
$ secondlayer subgraphs create my-watcher --from-contract SP....my-contract
|
|
39599
39594
|
$ secondlayer subgraphs deploy subgraphs/my-watcher.ts
|
|
39600
39595
|
`);
|
|
39601
39596
|
program.commandsGroup("Getting started:");
|
|
@@ -39607,7 +39602,7 @@ registerConsoleCommand(program);
|
|
|
39607
39602
|
registerLoginCommand(program);
|
|
39608
39603
|
registerLogoutCommand(program);
|
|
39609
39604
|
registerWhoamiCommand(program);
|
|
39610
|
-
program.commandsGroup("
|
|
39605
|
+
program.commandsGroup("Your data:");
|
|
39611
39606
|
registerSubgraphsCommand(program);
|
|
39612
39607
|
registerSubscriptionsCommand(program);
|
|
39613
39608
|
registerStreamsCommand(program);
|
|
@@ -39637,9 +39632,12 @@ registerConfigCommand(program);
|
|
|
39637
39632
|
registerContextCommand(program);
|
|
39638
39633
|
registerVerifyCommand(program);
|
|
39639
39634
|
registerRepairCommand(program);
|
|
39640
|
-
program
|
|
39635
|
+
registerBackupCommand(program);
|
|
39636
|
+
registerRestoreCommand(program);
|
|
39637
|
+
registerUninstallCommand(program);
|
|
39638
|
+
program.commandsGroup("Archive:");
|
|
39641
39639
|
registerCreditsCommand(program);
|
|
39642
39640
|
program.parse();
|
|
39643
39641
|
|
|
39644
|
-
//# debugId=
|
|
39642
|
+
//# debugId=135579D3DFB23FDC64756E2164756E21
|
|
39645
39643
|
//# sourceMappingURL=cli.js.map
|