Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/commands/network/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export function initializeNetworkCommands(program: Command) {
.option("--rounds-storage <addr>", "RoundsStorage contract address override")
.option("--appeals <addr>", "Appeals contract address override")
.option("--chain-id <n>", "Chain ID override")
.option("--explorer <url>", "Block explorer URL for this custom network (custom networks do NOT inherit the base's explorer, to avoid misleading links)")
.action((alias: string, options) => networkActions.addNetwork(alias, options));

// genlayer network set [name]
Expand Down
12 changes: 11 additions & 1 deletion src/commands/network/setNetwork.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export interface AddNetworkOptions {
roundsStorage?: string;
appeals?: string;
chainId?: string;
explorer?: string;
}

type NetworkEntry =
Expand Down Expand Up @@ -174,10 +175,11 @@ export class NetworkActions extends BaseAction {
options.deployment ||
options.rpc ||
options.chainId !== undefined ||
options.explorer ||
CONTRACT_FLAG_OPTIONS.some(option => Boolean(options[option.optionKey])),
);
if (!hasOverrideInput) {
throw new Error("Provide at least one override: --deployment, --rpc, --chain-id, or a contract address flag");
throw new Error("Provide at least one override: --deployment, --rpc, --chain-id, --explorer, or a contract address flag");
}

const overrides: CustomNetworkOverrides = {};
Expand Down Expand Up @@ -210,6 +212,14 @@ export class NetworkActions extends BaseAction {
overrides.chainId = chainId;
}

if (options.explorer) {
const url = options.explorer.trim();
if (!/^https?:\/\//i.test(url)) {
throw new Error(`Invalid --explorer URL (must start with http:// or https://): ${options.explorer}`);
}
overrides.explorer = url;
}

return {
base: options.base,
overrides,
Expand Down
19 changes: 18 additions & 1 deletion src/lib/networks/customNetworks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ export interface CustomNetworkOverrides {
feeManager?: Address;
roundsStorage?: Address;
appeals?: Address;
/** Block explorer URL for this custom network. Custom profiles do NOT inherit the base chain's explorer (see applyCustomNetworkProfile). */
explorer?: string;
}

export interface CustomNetworkProfile {
Expand Down Expand Up @@ -97,7 +99,9 @@ export function parseDeploymentObject(input: unknown, deploymentKey?: string): P
}

const found: Record<string, FoundDeploymentAddress[]> = {};
walkDeploymentObject(selected, [], found);
// Guarded above (non-null, object, non-array); narrow from `object` to the
// record shape walkDeploymentObject expects.
walkDeploymentObject(selected as Record<string, unknown>, [], found);

for (const [contractName, entries] of Object.entries(found)) {
if (entries.length > 1) {
Expand Down Expand Up @@ -179,6 +183,19 @@ export function applyCustomNetworkProfile(
};
}

// Custom profiles do NOT inherit the base chain's block explorer. The base's
// explorer indexes the BASE deployment, so surfacing it for a custom network
// is misleading — e.g. a pre-clarke profile based on bradbury would show
// explorer-bradbury links that never indexed its transactions. Show an
// explorer only when the operator set one explicitly via --explorer.
if (overrides.explorer) {
(chain as any).blockExplorers = {
default: {name: "Explorer", url: overrides.explorer},
};
} else {
delete (chain as any).blockExplorers;
}

return chain;
}

Expand Down
46 changes: 44 additions & 2 deletions tests/actions/customNetworkProfiles.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,48 @@ describe("custom network profiles", () => {
);
});

test("network add stores and applies an --explorer override", async () => {
await action.addNetwork("bradbury-explorer", {
base: "testnet-bradbury",
rpc: "http://localhost:9999",
explorer: "https://explorer.custom.example/",
});

expect(failSpy).not.toHaveBeenCalled();
expect(readConfig().customNetworks["bradbury-explorer"].overrides.explorer).toBe(
"https://explorer.custom.example/",
);
const chain = resolveNetwork("bradbury-explorer", readConfig().customNetworks);
expect(chain.blockExplorers?.default?.url).toBe("https://explorer.custom.example/");
});

test("custom network does NOT inherit the base block explorer when --explorer is omitted", async () => {
// Guard the premise: the base chain does carry an explorer.
expect(testnetBradbury.blockExplorers?.default?.url).toBeTruthy();

await action.addNetwork("bradbury-no-explorer", {
base: "testnet-bradbury",
rpc: "http://localhost:9999",
});

expect(failSpy).not.toHaveBeenCalled();
const chain = resolveNetwork("bradbury-no-explorer", readConfig().customNetworks);
// The misleading base explorer must NOT be inherited.
expect(chain.blockExplorers).toBeUndefined();
});

test("network add rejects an invalid --explorer URL", async () => {
await action.addNetwork("bradbury-bad-explorer", {
base: "testnet-bradbury",
explorer: "explorer.custom.example",
});

expect(failSpy).toHaveBeenCalledWith(
"Failed to add custom network profile",
expect.stringContaining("Invalid --explorer URL"),
);
});

test("network add sources overrides from a deployment file", async () => {
const deploymentPath = writeDeployment({
genlayerTestnet: {
Expand Down Expand Up @@ -145,7 +187,7 @@ describe("custom network profiles", () => {
expect(failSpy).toHaveBeenNthCalledWith(
4,
"Failed to add custom network profile",
"Provide at least one override: --deployment, --rpc, --chain-id, or a contract address flag",
"Provide at least one override: --deployment, --rpc, --chain-id, --explorer, or a contract address flag",
);
});

Expand Down Expand Up @@ -268,7 +310,7 @@ describe("custom network profiles", () => {

test("StakingAction.getNetwork accepts a custom alias", () => {
const stakingAction = new StakingAction();
vi.spyOn(stakingAction as any, "getConfigByKey").mockImplementation((key: string) => {
(vi.spyOn(stakingAction as any, "getConfigByKey") as any).mockImplementation((key: string) => {
if (key === "customNetworks") {
return {
"bradbury-clarke": {
Expand Down
Loading