Skip to content

Commit cd6ef11

Browse files
authored
feat: publish subgraph hash configmap (#39)
## Summary - add a helper to load and validate the subgraph hash from a file - include the subgraph hash ConfigMap in file and Kubernetes outputs with a default name - wire the generate command to read the optional subgraph hash file and extend tests ## Testing - bun test - bun run typecheck - bun run check ------ https://chatgpt.com/codex/tasks/task_e_68cf2d48dc44833287e31391dee5278e ## Summary by Sourcery Add support for loading and publishing a subgraph IPFS hash as a ConfigMap in the network bootstrap process. New Features: - Introduce loadSubgraphHash helper to read and validate an IPFS hash from a file - Add --subgraph-hash-file CLI option and SUBGRAPH_HASH_FILE environment variable to specify the subgraph hash file - Include subgraph hash in payload and publish it as a ConfigMap in both file and Kubernetes outputs Build: - Add multiformats dependency for IPFS CID parsing Documentation: - Document the new --subgraph-hash-file option in the README Tests: - Add unit tests for loadSubgraphHash to validate file existence, emptiness, and CID format - Extend bootstrap command and output tests to cover subgraph hash loading and ConfigMap generation
1 parent c436033 commit cd6ef11

10 files changed

Lines changed: 243 additions & 1 deletion

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ Options:
4242
-v, --validators <count> Number of validator nodes to generate. (default: 4)
4343
-a, --allocations <file> Path to a genesis allocations JSON file. (default: none)
4444
--abi-directory <path> Directory containing ABI JSON files to publish as ConfigMaps.
45+
--subgraph-hash-file <path> Path to a file containing the subgraph IPFS hash.
4546
-o, --outputType <type> Output target (screen, file, kubernetes). (default: "screen")
4647
--static-node-port <number> P2P port used for static-nodes enode URIs. (default: 30303)
4748
--static-node-discovery-port <number> Discovery port used for static-nodes enode URIs. (default: 30303)

bun.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"@inquirer/prompts": "7.8.6",
4242
"@kubernetes/client-node": "1.3.0",
4343
"commander": "14.0.1",
44+
"multiformats": "12.1.3",
4445
"lefthook": "1.13.1",
4546
"ox": "0.9.6",
4647
"viem": "2.37.7",

src/cli/commands/bootstrap/bootstrap.command.test.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const {
2323
genesisConfigMapName: DEFAULT_GENESIS_CONFIGMAP_NAME,
2424
staticNodesConfigMapName: DEFAULT_STATIC_NODES_CONFIGMAP_NAME,
2525
faucetArtifactPrefix: DEFAULT_FAUCET_PREFIX,
26+
subgraphConfigMapName: DEFAULT_SUBGRAPH_CONFIGMAP_NAME,
2627
} = ARTIFACT_DEFAULTS;
2728
const UNCOMPRESSED_PUBLIC_KEY_PREFIX = "04";
2829
const UNCOMPRESSED_PUBLIC_KEY_LENGTH = 130;
@@ -35,6 +36,8 @@ const PUBLIC_KEY_REPEAT = 64;
3536
const FIRST_VALIDATOR_INDEX = 1;
3637
const SECOND_VALIDATOR_INDEX = 2;
3738
const FAUCET_INDEX = VALIDATOR_RETURN + 1;
39+
const SAMPLE_SUBGRAPH_HASH =
40+
"bafybeigdyrztzd4gufq2bdsd6we3jh7uzulnd2ipkyli5sto6f5j6rlude";
3841
const createFactoryStub = () => {
3942
let counter = 0;
4043
return {
@@ -204,6 +207,7 @@ describe("CLI command bootstrap", () => {
204207
loadAbisPath = path;
205208
return Promise.resolve([]);
206209
},
210+
loadSubgraphHash: () => Promise.resolve(SAMPLE_SUBGRAPH_HASH),
207211
outputResult: async (type, payload) => {
208212
outputInvocation = { type, payload };
209213
await realOutputResult(type, payload);
@@ -242,6 +246,7 @@ describe("CLI command bootstrap", () => {
242246
validatorPrefix: DEFAULT_POD_PREFIX,
243247
genesisConfigMapName: DEFAULT_GENESIS_CONFIGMAP_NAME,
244248
staticNodesConfigMapName: DEFAULT_STATIC_NODES_CONFIGMAP_NAME,
249+
subgraphConfigMapName: DEFAULT_SUBGRAPH_CONFIGMAP_NAME,
245250
});
246251
});
247252

@@ -278,6 +283,7 @@ describe("CLI command bootstrap", () => {
278283
capturedAbiPath = path;
279284
return Promise.resolve(abiArtifacts);
280285
},
286+
loadSubgraphHash: () => Promise.resolve(SAMPLE_SUBGRAPH_HASH),
281287
outputResult: (_type, payload) => {
282288
capturedPayload = payload;
283289
return Promise.resolve();
@@ -296,6 +302,56 @@ describe("CLI command bootstrap", () => {
296302
expect(capturedPayload?.abiArtifacts).toEqual(abiArtifacts);
297303
});
298304

305+
test("runBootstrap loads subgraph hash from environment", async () => {
306+
const factory = createFactoryStub();
307+
let capturedPath: string | undefined;
308+
let capturedPayload: OutputPayload | undefined;
309+
const originalEnv = Bun.env.SUBGRAPH_HASH_FILE;
310+
311+
const deps: BootstrapDependencies = {
312+
factory,
313+
promptForCount: () => Promise.resolve(EXPECTED_DEFAULT_VALIDATOR),
314+
promptForGenesis: async (_service, { faucetAddress }) => ({
315+
algorithm: ALGORITHM.QBFT,
316+
config: {
317+
chainId: 1,
318+
faucetWalletAddress: faucetAddress,
319+
gasLimit: "0x1",
320+
secondsPerBlock: 2,
321+
},
322+
genesis: { config: {}, extraData: "0x" } as any,
323+
}),
324+
promptForText: passthroughTextPrompt,
325+
service: {} as any,
326+
loadAllocations: () =>
327+
Promise.resolve({} satisfies Record<string, BesuAllocAccount>),
328+
loadAbis: () => Promise.resolve([]),
329+
loadSubgraphHash: (path: string) => {
330+
capturedPath = path;
331+
return Promise.resolve(SAMPLE_SUBGRAPH_HASH);
332+
},
333+
outputResult: (_type, payload) => {
334+
capturedPayload = payload;
335+
return Promise.resolve();
336+
},
337+
};
338+
339+
Bun.env.SUBGRAPH_HASH_FILE = " /tmp/subgraph.txt ";
340+
341+
try {
342+
await runBootstrap({ acceptDefaults: true }, deps);
343+
} finally {
344+
if (originalEnv === undefined) {
345+
Bun.env.SUBGRAPH_HASH_FILE = undefined;
346+
} else {
347+
Bun.env.SUBGRAPH_HASH_FILE = originalEnv;
348+
}
349+
}
350+
351+
expect(capturedPath).toBe("/tmp/subgraph.txt");
352+
expect(capturedPayload?.subgraphHash).toBe(SAMPLE_SUBGRAPH_HASH);
353+
});
354+
299355
test("createCliCommand wires metadata", () => {
300356
const command = createCliCommand();
301357
expect(command.name()).toBe("network-bootstrapper");
@@ -337,6 +393,7 @@ describe("CLI command bootstrap", () => {
337393
loadAllocations: () =>
338394
Promise.resolve({} satisfies Record<string, BesuAllocAccount>),
339395
loadAbis: () => Promise.resolve([]),
396+
loadSubgraphHash: () => Promise.resolve(SAMPLE_SUBGRAPH_HASH),
340397
outputResult: async (type, payload) => {
341398
await realOutputResult(type, payload);
342399
},
@@ -405,6 +462,7 @@ describe("CLI command bootstrap", () => {
405462
loadAllocations: () =>
406463
Promise.resolve({} satisfies Record<string, BesuAllocAccount>),
407464
loadAbis: () => Promise.resolve([]),
465+
loadSubgraphHash: () => Promise.resolve(SAMPLE_SUBGRAPH_HASH),
408466
outputResult: (_type, payload) => {
409467
capturedPayload = payload;
410468
return Promise.resolve();
@@ -457,6 +515,7 @@ describe("CLI command bootstrap", () => {
457515
validatorPrefix: "custom-validator",
458516
genesisConfigMapName: "custom-genesis",
459517
staticNodesConfigMapName: "custom-static-nodes",
518+
subgraphConfigMapName: DEFAULT_SUBGRAPH_CONFIGMAP_NAME,
460519
});
461520
});
462521

@@ -487,6 +546,7 @@ describe("CLI command bootstrap", () => {
487546
loadAllocations: () =>
488547
Promise.resolve({} satisfies Record<string, BesuAllocAccount>),
489548
loadAbis: () => Promise.resolve([]),
549+
loadSubgraphHash: () => Promise.resolve(SAMPLE_SUBGRAPH_HASH),
490550
outputResult: (_type, payload) => {
491551
capturedPayload = payload;
492552
return Promise.resolve();
@@ -556,6 +616,7 @@ describe("CLI command bootstrap", () => {
556616
loadAllocations: () =>
557617
Promise.resolve({} satisfies Record<string, BesuAllocAccount>),
558618
loadAbis: () => Promise.resolve([]),
619+
loadSubgraphHash: () => Promise.resolve(SAMPLE_SUBGRAPH_HASH),
559620
outputResult: async () => {
560621
// no-op for test
561622
},
@@ -639,6 +700,7 @@ describe("CLI command bootstrap", () => {
639700
loadAllocations: () =>
640701
Promise.resolve({} satisfies Record<string, BesuAllocAccount>),
641702
loadAbis: () => Promise.resolve([]),
703+
loadSubgraphHash: () => Promise.resolve(SAMPLE_SUBGRAPH_HASH),
642704
outputResult: (type) => {
643705
capturedOutputType = type;
644706
return Promise.resolve();
@@ -730,13 +792,15 @@ describe("CLI command bootstrap", () => {
730792
loadAbisInvoked = true;
731793
return Promise.resolve([]);
732794
},
795+
loadSubgraphHash: () => Promise.resolve(SAMPLE_SUBGRAPH_HASH),
733796
outputResult: (_type, payload) => {
734797
expect(payload.validators).toHaveLength(EXPECTED_DEFAULT_VALIDATOR);
735798
expect(payload.artifactNames).toEqual({
736799
faucetPrefix: DEFAULT_FAUCET_PREFIX,
737800
validatorPrefix: DEFAULT_POD_PREFIX,
738801
genesisConfigMapName: DEFAULT_GENESIS_CONFIGMAP_NAME,
739802
staticNodesConfigMapName: DEFAULT_STATIC_NODES_CONFIGMAP_NAME,
803+
subgraphConfigMapName: DEFAULT_SUBGRAPH_CONFIGMAP_NAME,
740804
});
741805
return Promise.resolve();
742806
},

src/cli/commands/bootstrap/bootstrap.command.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
promptForCount,
2626
promptForText,
2727
} from "./bootstrap.prompt-helpers.ts";
28+
import { loadSubgraphHash } from "./bootstrap.subgraph.ts";
2829

2930
type CliOptions = {
3031
allocations?: string;
@@ -48,6 +49,7 @@ type CliOptions = {
4849
genesisConfigmapName?: string;
4950
staticNodesConfigmapName?: string;
5051
faucetArtifactPrefix?: string;
52+
subgraphHashFile?: string;
5153
};
5254

5355
type BootstrapDependencies = {
@@ -58,6 +60,7 @@ type BootstrapDependencies = {
5860
service: BesuGenesisService;
5961
loadAllocations: typeof loadAllocations;
6062
loadAbis: typeof loadAbis;
63+
loadSubgraphHash: typeof loadSubgraphHash;
6164
outputResult: (type: OutputType, payload: OutputPayload) => Promise<void>;
6265
};
6366

@@ -69,6 +72,7 @@ const {
6972
genesisConfigMapName: DEFAULT_GENESIS_CONFIGMAP_NAME,
7073
staticNodesConfigMapName: DEFAULT_STATIC_NODES_CONFIGMAP_NAME,
7174
faucetArtifactPrefix: DEFAULT_FAUCET_ARTIFACT_PREFIX,
75+
subgraphConfigMapName: DEFAULT_SUBGRAPH_CONFIGMAP_NAME,
7276
} = ARTIFACT_DEFAULTS;
7377
const OUTPUT_CHOICES: OutputType[] = ["screen", "file", "kubernetes"];
7478
const LEADING_DOT_REGEX = /^\./u;
@@ -303,6 +307,7 @@ const runBootstrap = async (
303307
genesisConfigmapName: genesisConfigmapNameOption,
304308
staticNodesConfigmapName: staticNodesConfigmapNameOption,
305309
faucetArtifactPrefix: faucetArtifactPrefixOption,
310+
subgraphHashFile: subgraphHashFileOption,
306311
} = options;
307312

308313
const resolveCount = (
@@ -399,6 +404,22 @@ const runBootstrap = async (
399404
? await deps.loadAbis(trimmedAbiDirectory)
400405
: [];
401406

407+
const envSubgraphHashFile = Bun.env.SUBGRAPH_HASH_FILE?.trim();
408+
const providedSubgraphHashFile =
409+
subgraphHashFileOption === undefined
410+
? undefined
411+
: subgraphHashFileOption.trim();
412+
let subgraphHashPath: string | undefined;
413+
if (providedSubgraphHashFile && providedSubgraphHashFile.length > 0) {
414+
subgraphHashPath = providedSubgraphHashFile;
415+
} else if (envSubgraphHashFile && envSubgraphHashFile.length > 0) {
416+
subgraphHashPath = envSubgraphHashFile;
417+
}
418+
419+
const subgraphHash = subgraphHashPath
420+
? await deps.loadSubgraphHash(subgraphHashPath)
421+
: undefined;
422+
402423
const { genesis } = await deps.promptForGenesis(deps.service, {
403424
faucetAddress,
404425
allocations: allocationOverrides,
@@ -425,8 +446,10 @@ const runBootstrap = async (
425446
validatorPrefix: staticNodePodPrefix,
426447
genesisConfigMapName,
427448
staticNodesConfigMapName,
449+
subgraphConfigMapName: DEFAULT_SUBGRAPH_CONFIGMAP_NAME,
428450
},
429451
abiArtifacts,
452+
subgraphHash,
430453
};
431454

432455
await deps.outputResult(outputType ?? "screen", payload);
@@ -441,6 +464,7 @@ const defaultDependencies: BootstrapDependencies = {
441464
service: new BesuGenesisService(),
442465
loadAllocations,
443466
loadAbis,
467+
loadSubgraphHash,
444468
outputResult: defaultOutputResult,
445469
};
446470
/* c8 ignore end */
@@ -487,6 +511,11 @@ const createCliCommand = (
487511
"Directory containing ABI JSON files to publish as ConfigMaps.",
488512
(value: string) => stripSurroundingQuotes(value)
489513
)
514+
.option(
515+
"--subgraph-hash-file <path>",
516+
"Path to a file containing the subgraph IPFS hash.",
517+
(value: string) => stripSurroundingQuotes(value)
518+
)
490519
.option(
491520
"-o, --outputType <type>",
492521
`Output target (${OUTPUT_CHOICES.join(", ")}).`,
@@ -601,6 +630,10 @@ const createCliCommand = (
601630
normalizedOptions.abiDirectory === undefined
602631
? undefined
603632
: stripSurroundingQuotes(normalizedOptions.abiDirectory),
633+
subgraphHashFile:
634+
normalizedOptions.subgraphHashFile === undefined
635+
? undefined
636+
: stripSurroundingQuotes(normalizedOptions.subgraphHashFile),
604637
};
605638

606639
for (const { key, sanitize } of TEXT_OPTION_DESCRIPTORS) {
@@ -628,6 +661,12 @@ const createCliCommand = (
628661
trimmed.length === 0 ? undefined : trimmed;
629662
}
630663

664+
if (sanitizedOptions.subgraphHashFile) {
665+
const trimmed = sanitizedOptions.subgraphHashFile.trim();
666+
sanitizedOptions.subgraphHashFile =
667+
trimmed.length === 0 ? undefined : trimmed;
668+
}
669+
631670
await runBootstrap(sanitizedOptions, deps);
632671
});
633672

0 commit comments

Comments
 (0)