From da816a86697b27bab8d7da731276ef07e02b7dfe Mon Sep 17 00:00:00 2001 From: jmlago Date: Mon, 19 Oct 2026 19:30:47 +0100 Subject: [PATCH 1/8] fix: bundle Bradbury Phase 2 SDK and isolate validator configuration --- README.md | 4 ++++ package-lock.json | 7 +++---- package.json | 2 +- src/lib/actions/BaseAction.ts | 7 +++++++ src/lib/config/ConfigFileManager.ts | 2 +- tests/actions/staking.test.ts | 9 +++++++++ tests/libs/configFileManager.test.ts | 9 +++++++++ 7 files changed, 34 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 2658970e..9700b6c2 100644 --- a/README.md +++ b/README.md @@ -718,3 +718,7 @@ We welcome contributions to GenLayerJS SDK! Whether it's new features, improved ## License This project is licensed under the ... License - see the [LICENSE](LICENSE) file for details. + +### Isolated validator configuration + +Set `GENLAYER_CONFIG_DIR` to an absolute directory to keep a validator qualification account and network configuration separate from your normal `~/.genlayer` configuration. The directory and its keystore subdirectory are created with mode 0700. diff --git a/package-lock.json b/package-lock.json index f0586ad5..7c4b8d45 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,7 +46,7 @@ "eslint-config-prettier": "^10.0.0", "eslint-import-resolver-typescript": "^4.0.0", "eslint-plugin-import": "^2.29.1", - "genlayer-js": "github:genlayerlabs/genlayer-js#facd9e9dc9a289d0110fe3b5b1a14a2938fe6e01", + "genlayer-js": "github:genlayerlabs/genlayer-js#fa765e1fda1d3d48181b88dfd3f6e4f29179ae03", "jsdom": "^26.0.0", "prettier": "^3.2.5", "release-it": "^19.0.0", @@ -5682,9 +5682,8 @@ } }, "node_modules/genlayer-js": { - "version": "1.1.8", - "resolved": "git+ssh://git@github.com/genlayerlabs/genlayer-js.git#facd9e9dc9a289d0110fe3b5b1a14a2938fe6e01", - "integrity": "sha512-ExvojIzcD0bVUMMJ0ir0dUdnXfPefLPEH92vfGWW2CSUxAGdeHq0tu16PSbIITwdib1bV9cr+Qmbp6JDAVz9Jw==", + "version": "2.0.0-rc.1", + "resolved": "git+ssh://git@github.com/genlayerlabs/genlayer-js.git#fa765e1fda1d3d48181b88dfd3f6e4f29179ae03", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 12bd5327..30e4369c 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "eslint-config-prettier": "^10.0.0", "eslint-import-resolver-typescript": "^4.0.0", "eslint-plugin-import": "^2.29.1", - "genlayer-js": "github:genlayerlabs/genlayer-js#facd9e9dc9a289d0110fe3b5b1a14a2938fe6e01", + "genlayer-js": "github:genlayerlabs/genlayer-js#fa765e1fda1d3d48181b88dfd3f6e4f29179ae03", "jsdom": "^26.0.0", "prettier": "^3.2.5", "release-it": "^19.0.0", diff --git a/src/lib/actions/BaseAction.ts b/src/lib/actions/BaseAction.ts index d8c08a86..0b7b653b 100644 --- a/src/lib/actions/BaseAction.ts +++ b/src/lib/actions/BaseAction.ts @@ -620,6 +620,13 @@ export class BaseAction extends ConfigFileManager { } = params; if (currentEpoch === 0n) { + if (resultingSelfStakeRaw >= minStakeRaw) { + this.logInfo( + "Epoch 0: self-stake meets the current minimum. Registration does not imply activation; " + + "check the validator activation epoch and priming status.", + ); + return; + } this.logInfo( `Epoch 0: minimum self-stake not enforced yet. Note: this validator won't become active ` + `until its self-stake reaches ${minStakeFormatted}.`, diff --git a/src/lib/config/ConfigFileManager.ts b/src/lib/config/ConfigFileManager.ts index 48e7564e..659aea81 100644 --- a/src/lib/config/ConfigFileManager.ts +++ b/src/lib/config/ConfigFileManager.ts @@ -13,7 +13,7 @@ export class ConfigFileManager { private configFilePath: string; private keystoresPath: string; - constructor(baseFolder: string = ".genlayer/", configFileName: string = "genlayer-config.json") { + constructor(baseFolder: string = process.env.GENLAYER_CONFIG_DIR || ".genlayer/", configFileName: string = "genlayer-config.json") { this.folderPath = path.resolve(os.homedir(), baseFolder); this.configFilePath = path.resolve(this.folderPath, configFileName); this.keystoresPath = path.resolve(this.folderPath, "keystores"); diff --git a/tests/actions/staking.test.ts b/tests/actions/staking.test.ts index 5ee03ff6..6af2cc0a 100644 --- a/tests/actions/staking.test.ts +++ b/tests/actions/staking.test.ts @@ -1118,6 +1118,15 @@ describe("ValidatorJoinAction self-stake minimum gate", () => { expect(mockClient.validatorJoin).toHaveBeenCalledTimes(1); expect(action["failSpinner"]).not.toHaveBeenCalled(); }); + + test("reports a satisfied epoch-zero minimum without claiming activation", async () => { + mockClient.getEpochInfo.mockResolvedValue(epoch0EpochInfo); + const info = vi.spyOn(action as any, "logInfo").mockImplementation(() => {}); + await action.execute({amount: "50000gen", stakingAddress: "0xStaking"}); + expect(mockClient.validatorJoin).toHaveBeenCalledTimes(1); + expect(info).toHaveBeenCalledWith(expect.stringContaining("self-stake meets the current minimum")); + expect(info).toHaveBeenCalledWith(expect.stringContaining("Registration does not imply activation")); + }); }); describe("ValidatorDepositAction minimum gate + mixing guard", () => { diff --git a/tests/libs/configFileManager.test.ts b/tests/libs/configFileManager.test.ts index e92c5d47..4cd410e0 100644 --- a/tests/libs/configFileManager.test.ts +++ b/tests/libs/configFileManager.test.ts @@ -26,6 +26,7 @@ describe("ConfigFileManager", () => { afterEach(() => { vi.restoreAllMocks(); + vi.unstubAllEnvs(); }); test("ensures folder and config file are created if they don't exist", () => { @@ -57,6 +58,14 @@ describe("ConfigFileManager", () => { expect(fs.mkdirSync).not.toHaveBeenCalled(); }); + test("isolates qualification accounts using GENLAYER_CONFIG_DIR", () => { + vi.stubEnv("GENLAYER_CONFIG_DIR", "/tmp/genlayer-qualification/config"); + const isolated = new ConfigFileManager(); + expect(isolated.getFolderPath()).toBe("/tmp/genlayer-qualification/config"); + expect(isolated.getKeystorePath("operator")).toBe("/tmp/genlayer-qualification/config/keystores/operator.json"); + expect(new ConfigFileManager("explicit").getFolderPath()).toBe(path.resolve(mockHome, "explicit")); + }); + test("getFolderPath returns the correct folder path", () => { expect(configFileManager.getFolderPath()).toBe(mockFolderPath); }); From 1e2533966a73e175e62b0f8fadbde1d94242ab24 Mon Sep 17 00:00:00 2001 From: jmlago Date: Mon, 19 Oct 2026 19:43:29 +0100 Subject: [PATCH 2/8] fix: validate registry configuration and keep Windows tests portable --- .gitattributes | 2 ++ package-lock.json | 4 ++-- package.json | 2 +- tests/libs/configFileManager.test.ts | 7 ++++--- 4 files changed, 9 insertions(+), 6 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..72f1be05 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Keep hashbang-bearing JavaScript importable by Node/Vitest on Windows. +*.mjs text eol=lf diff --git a/package-lock.json b/package-lock.json index 7c4b8d45..5771e52a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,7 +46,7 @@ "eslint-config-prettier": "^10.0.0", "eslint-import-resolver-typescript": "^4.0.0", "eslint-plugin-import": "^2.29.1", - "genlayer-js": "github:genlayerlabs/genlayer-js#fa765e1fda1d3d48181b88dfd3f6e4f29179ae03", + "genlayer-js": "github:genlayerlabs/genlayer-js#7e29c039aba5febcd0965d98c991c793bf975ff8", "jsdom": "^26.0.0", "prettier": "^3.2.5", "release-it": "^19.0.0", @@ -5683,7 +5683,7 @@ }, "node_modules/genlayer-js": { "version": "2.0.0-rc.1", - "resolved": "git+ssh://git@github.com/genlayerlabs/genlayer-js.git#fa765e1fda1d3d48181b88dfd3f6e4f29179ae03", + "resolved": "git+ssh://git@github.com/genlayerlabs/genlayer-js.git#7e29c039aba5febcd0965d98c991c793bf975ff8", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 30e4369c..dd578434 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "eslint-config-prettier": "^10.0.0", "eslint-import-resolver-typescript": "^4.0.0", "eslint-plugin-import": "^2.29.1", - "genlayer-js": "github:genlayerlabs/genlayer-js#fa765e1fda1d3d48181b88dfd3f6e4f29179ae03", + "genlayer-js": "github:genlayerlabs/genlayer-js#7e29c039aba5febcd0965d98c991c793bf975ff8", "jsdom": "^26.0.0", "prettier": "^3.2.5", "release-it": "^19.0.0", diff --git a/tests/libs/configFileManager.test.ts b/tests/libs/configFileManager.test.ts index 4cd410e0..d1eddf6b 100644 --- a/tests/libs/configFileManager.test.ts +++ b/tests/libs/configFileManager.test.ts @@ -59,10 +59,11 @@ describe("ConfigFileManager", () => { }); test("isolates qualification accounts using GENLAYER_CONFIG_DIR", () => { - vi.stubEnv("GENLAYER_CONFIG_DIR", "/tmp/genlayer-qualification/config"); + const isolatedPath = path.resolve(mockHome, "qualification", "config"); + vi.stubEnv("GENLAYER_CONFIG_DIR", isolatedPath); const isolated = new ConfigFileManager(); - expect(isolated.getFolderPath()).toBe("/tmp/genlayer-qualification/config"); - expect(isolated.getKeystorePath("operator")).toBe("/tmp/genlayer-qualification/config/keystores/operator.json"); + expect(isolated.getFolderPath()).toBe(isolatedPath); + expect(isolated.getKeystorePath("operator")).toBe(path.join(isolatedPath, "keystores", "operator.json")); expect(new ConfigFileManager("explicit").getFolderPath()).toBe(path.resolve(mockHome, "explicit")); }); From 674ca028058f88a7888fe4a315b7c196ae4a8fa2 Mon Sep 17 00:00:00 2001 From: Darien Hernandez Date: Mon, 19 Oct 2026 23:37:41 +0200 Subject: [PATCH 3/8] fix(staking): replace removed ValidatorInfo.live with hasUnclaimedRewards genlayer-js 2.0.0-rc.1 drops ValidatorInfo.live (no alias; see its consumer-migration-v06 note). esbuild does not type-check and tests mock the SDK, so the build stayed green while validator-info printed 'live: undefined' and every primed, non-active validator showed 'inactive'. Surface the raw hasUnclaimedRewards flag instead and derive 'pending' from a self-stake deposit that is still activating. Co-Authored-By: Claude Opus 5.5 --- src/commands/staking/stakingInfo.ts | 9 ++--- src/commands/staking/validators.ts | 11 +++---- tests/actions/staking.test.ts | 13 ++++---- tests/commands/stakingValidators.test.ts | 42 +++++++++++++++++++----- tests/smoke.test.ts | 2 +- 5 files changed, 51 insertions(+), 26 deletions(-) diff --git a/src/commands/staking/stakingInfo.ts b/src/commands/staking/stakingInfo.ts index e1cfcc28..086a23e8 100644 --- a/src/commands/staking/stakingInfo.ts +++ b/src/commands/staking/stakingInfo.ts @@ -52,7 +52,7 @@ export class StakingInfoAction extends StakingAction { vWithdrawal: info.vWithdrawal, ePrimed: info.ePrimed.toString(), needsPriming: info.needsPriming, - live: info.live, + hasUnclaimedRewards: info.hasUnclaimedRewards, banned: info.banned ? info.bannedEpoch?.toString() : "Not banned", selfStakePendingDeposits: (() => { // In debug mode, show all deposits; otherwise filter to truly pending only @@ -194,9 +194,9 @@ export class StakingInfoAction extends StakingAction { console.log(` ${label("Withdrawal (vWithdrawal):")} ${info.vWithdrawal}`); console.log(""); - // Status. `live` printed as the raw boolean, an opaque contract flag. + // Status. `hasUnclaimedRewards` printed as the raw boolean, an opaque contract flag. console.log(chalk.bold("Status")); - console.log(` ${label("live:")} ${info.live} ${chalk.gray("(contract flag: primed & not exited — not an eligibility signal)")}`); + console.log(` ${label("hasUnclaimedRewards:")} ${info.hasUnclaimedRewards} ${chalk.gray("(rewards still to be consumed at priming — not an eligibility signal)")}`); console.log(` ${label("needsPriming:")} ${info.needsPriming}`); console.log(` ${label("Banned:")} ${result.banned}`); console.log(` ${label("Primed epoch (ePrimed):")} ${info.ePrimed}`); @@ -625,7 +625,8 @@ export class StakingInfoAction extends StakingAction { } else if (info.needsPriming) { status = "needs-priming"; } else { - status = info.live ? "pending" : "inactive"; + const activating = info.pendingDeposits.some(d => d.epoch + ACTIVATION_DELAY_EPOCHS > epochInfo.currentEpoch); + status = activating ? "pending" : "inactive"; } const isMine = myAddress diff --git a/src/commands/staking/validators.ts b/src/commands/staking/validators.ts index 81313d21..80f9e7e8 100644 --- a/src/commands/staking/validators.ts +++ b/src/commands/staking/validators.ts @@ -73,7 +73,7 @@ interface ValidatorRow { operator: Address; moniker?: string; active: boolean; - live: boolean; + hasUnclaimedRewards: boolean; banned: boolean; bannedUntilEpoch?: string; status: string; @@ -230,6 +230,7 @@ export class ValidatorsAction extends StakingAction { let bannedUntilEpoch: string | undefined; const belowMin = info.vStakeRaw < validatorMinStakeRaw; const totalStakeRaw = info.vStakeRaw + info.dStakeRaw; + const trulyPendingDeposits = info.pendingDeposits.filter(d => d.epoch + ACTIVATION_DELAY_EPOCHS > currentEpoch); if (isBanned) { if (bannedInfo?.permanentlyBanned) { @@ -252,10 +253,8 @@ export class ValidatorsAction extends StakingAction { } else if (info.needsPriming) { status = "needs-priming"; } else { - status = info.live ? "pending" : "inactive"; + status = trulyPendingDeposits.length > 0 ? "pending" : "inactive"; } - - const trulyPendingDeposits = info.pendingDeposits.filter(d => d.epoch + ACTIVATION_DELAY_EPOCHS > currentEpoch); const trulyPendingWithdrawals = info.pendingWithdrawals.filter(w => w.epoch + UNBONDING_PERIOD_EPOCHS > currentEpoch); const isMine = myAddress @@ -269,7 +268,7 @@ export class ValidatorsAction extends StakingAction { operator: info.operator, moniker: info.identity?.moniker || undefined, active: isActive, - live: info.live, + hasUnclaimedRewards: info.hasUnclaimedRewards, banned: isBanned, bannedUntilEpoch, status, @@ -509,7 +508,7 @@ export class ValidatorsAction extends StakingAction { operator: row.operator, moniker: row.moniker || null, active: row.active, - live: row.live, + hasUnclaimedRewards: row.hasUnclaimedRewards, banned: row.banned, bannedUntilEpoch: row.bannedUntilEpoch || null, status: row.status, diff --git a/tests/actions/staking.test.ts b/tests/actions/staking.test.ts index 6af2cc0a..1109a692 100644 --- a/tests/actions/staking.test.ts +++ b/tests/actions/staking.test.ts @@ -580,7 +580,7 @@ describe("StakingInfoAction", () => { vWithdrawalRaw: 0n, ePrimed: 5n, needsPriming: false, - live: true, + hasUnclaimedRewards: false, banned: false, bannedEpoch: null, pendingDeposits: [], @@ -1063,7 +1063,7 @@ function fullValidatorInfo(overrides: Record = {}) { vWithdrawalRaw: 0n, ePrimed: 5n, needsPriming: false, - live: true, + hasUnclaimedRewards: false, banned: false, bannedEpoch: null, pendingDeposits: [], @@ -1217,7 +1217,8 @@ describe("StakingInfoAction clean view + eligibility display", () => { expect(action["succeedSpinner"]).not.toHaveBeenCalled(); const parsed = JSON.parse(logSpy.mock.calls[0][0]); expect(parsed.validator).toBe("0xValidatorWallet"); - expect(parsed.live).toBe(true); + expect(parsed.hasUnclaimedRewards).toBe(false); + expect(parsed).not.toHaveProperty("live"); expect(parsed.banned).toBe("Not banned"); }); @@ -1241,9 +1242,9 @@ describe("StakingInfoAction clean view + eligibility display", () => { expect(out).toContain("500 GEN"); // delegated amount expect(out).toContain("AcmeNode"); // moniker expect(out).toContain("Not banned"); - expect(out).toContain("live"); // the live label - expect(out).toContain("true"); // the live boolean value - // Never relabels live as "active". + expect(out).toContain("hasUnclaimedRewards"); // the raw contract flag label + expect(out).toContain("false"); // its boolean value + // Never relabels the flag as "active". expect(out).not.toContain("active"); }); diff --git a/tests/commands/stakingValidators.test.ts b/tests/commands/stakingValidators.test.ts index bbcf633d..3a0674fa 100644 --- a/tests/commands/stakingValidators.test.ts +++ b/tests/commands/stakingValidators.test.ts @@ -16,7 +16,7 @@ function validatorInfo( selfStake: number, delegatedStake: number, moniker: string, - options: {live?: boolean; needsPriming?: boolean} = {}, + options: {hasUnclaimedRewards?: boolean; needsPriming?: boolean; pendingDeposits?: any[]} = {}, ) { return { address, @@ -33,11 +33,11 @@ function validatorInfo( vWithdrawal: "0 GEN", vWithdrawalRaw: 0n, ePrimed: 5n, - live: options.live ?? true, + hasUnclaimedRewards: options.hasUnclaimedRewards ?? false, banned: false, needsPriming: options.needsPriming ?? false, identity: {moniker}, - pendingDeposits: [], + pendingDeposits: options.pendingDeposits ?? [], pendingWithdrawals: [], } as any; } @@ -52,19 +52,27 @@ function jsonResponse(body: unknown) { function createMockClient({ currentEpoch = 6n, validatorMinStakeRaw = rawGen(75), - betaLive = true, + betaHasUnclaimedRewards = false, + betaNeedsPriming = true, + betaPendingDeposits = [], betaSelfStake = 50, activeValidators = [A], }: { currentEpoch?: bigint; validatorMinStakeRaw?: bigint; - betaLive?: boolean; + betaHasUnclaimedRewards?: boolean; + betaNeedsPriming?: boolean; + betaPendingDeposits?: any[]; betaSelfStake?: number; activeValidators?: string[]; } = {}) { const infos = new Map([ [A.toLowerCase(), validatorInfo(A, 100, 20, "Alpha")], - [B.toLowerCase(), validatorInfo(B, betaSelfStake, 10, "Beta", {live: betaLive, needsPriming: true})], + [B.toLowerCase(), validatorInfo(B, betaSelfStake, 10, "Beta", { + hasUnclaimedRewards: betaHasUnclaimedRewards, + needsPriming: betaNeedsPriming, + pendingDeposits: betaPendingDeposits, + })], ]); return { @@ -137,8 +145,24 @@ describe("staking validators action", () => { expect(output.activeCount).toBe(1); }); + test("status is pending only while a self-stake deposit is still activating", async () => { + // Above the minimum, primed, not selected yet. `hasUnclaimedRewards` must not drive status. + const base = {betaSelfStake: 100, betaNeedsPriming: false, betaHasUnclaimedRewards: true}; + + let action = setupAction(createMockClient({...base, betaPendingDeposits: [{epoch: 5n, stakeRaw: rawGen(100)}]})); + await action.execute({json: true}); + let output = JSON.parse(logSpy.mock.calls.at(-1)?.[0] as string); + expect(output.validators[1].status).toBe("pending"); + expect(output.validators[1].hasUnclaimedRewards).toBe(true); + + action = setupAction(createMockClient({...base, betaPendingDeposits: [{epoch: 3n, stakeRaw: rawGen(100)}]})); + await action.execute({json: true}); + output = JSON.parse(logSpy.mock.calls.at(-1)?.[0] as string); + expect(output.validators[1].status).toBe("inactive"); + }); + test("keeps joined but non-selectable validators out of the active set", async () => { - const action = setupAction(createMockClient({betaSelfStake: 100, betaLive: true})); + const action = setupAction(createMockClient({betaSelfStake: 100, betaHasUnclaimedRewards: true})); await action.execute({json: true}); @@ -171,7 +195,7 @@ describe("staking validators action", () => { }); test("renders epoch 0 below-min validators as pending activation", async () => { - const action = setupAction(createMockClient({currentEpoch: 0n, betaLive: false})); + const action = setupAction(createMockClient({currentEpoch: 0n, betaHasUnclaimedRewards: false})); await action.execute({}); const output = loggedOutput(); @@ -182,7 +206,7 @@ describe("staking validators action", () => { }); test("renders epoch 2 below-min validators as inactive below-min", async () => { - const action = setupAction(createMockClient({currentEpoch: 2n, betaLive: false})); + const action = setupAction(createMockClient({currentEpoch: 2n, betaHasUnclaimedRewards: false})); await action.execute({}); const output = loggedOutput(); diff --git a/tests/smoke.test.ts b/tests/smoke.test.ts index 68ff4e71..7c9efc3f 100644 --- a/tests/smoke.test.ts +++ b/tests/smoke.test.ts @@ -103,7 +103,7 @@ describe(`Testnet ${name} - CLI Staking Smoke Tests`, () => { expect(typeof info.vShares).toBe("bigint"); expect(typeof info.dStake).toBe("string"); expect(typeof info.dStakeRaw).toBe("bigint"); - expect(typeof info.live).toBe("boolean"); + expect(typeof info.hasUnclaimedRewards).toBe("boolean"); expect(typeof info.banned).toBe("boolean"); expect(typeof info.needsPriming).toBe("boolean"); expect(Array.isArray(info.pendingDeposits)).toBe(true); From e93b0e58c47b01e5c2f474dbcbccd1bd1bb2a158 Mon Sep 17 00:00:00 2001 From: Darien Hernandez Date: Mon, 19 Oct 2026 23:37:41 +0200 Subject: [PATCH 4/8] fix(network): don't inherit base deployment identity in custom profiles The Phase 2 SDK checks genesisHash and AddressManager before every request. Custom profiles cloned them from the base chain, so any profile overriding contracts or the chain ID failed every call. Like the block explorer, drop them for such profiles; an --rpc-only profile (another provider for the same deployment) keeps the check. Co-Authored-By: Claude Opus 5.5 --- src/lib/networks/customNetworks.ts | 12 +++++++++++ tests/actions/customNetworkProfiles.test.ts | 23 +++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/src/lib/networks/customNetworks.ts b/src/lib/networks/customNetworks.ts index d8c624b3..602b1e4c 100644 --- a/src/lib/networks/customNetworks.ts +++ b/src/lib/networks/customNetworks.ts @@ -196,6 +196,18 @@ export function applyCustomNetworkProfile( delete (chain as any).blockExplorers; } + // The base chain's deployment identity (genesis hash + AddressManager) is + // checked by the SDK before every request. Like the explorer, it describes the + // BASE deployment: a profile that overrides contracts or the chain ID targets a + // different one, so an inherited identity would reject every call. Keep it only + // for an --rpc-only profile (another provider for the same deployment). + const targetsOtherDeployment = + overrides.chainId !== undefined || CONTRACT_OVERRIDES.some(contract => overrides[contract.overrideKey]); + if (targetsOtherDeployment) { + delete (chain as any).genesisHash; + delete (chain as any).addressManager; + } + return chain; } diff --git a/tests/actions/customNetworkProfiles.test.ts b/tests/actions/customNetworkProfiles.test.ts index e218ca27..6fd33057 100644 --- a/tests/actions/customNetworkProfiles.test.ts +++ b/tests/actions/customNetworkProfiles.test.ts @@ -323,6 +323,29 @@ describe("custom network profiles", () => { expect(resolvedChain.name).not.toBe(baseChain.name); }); + test("custom profile keeps the base deployment identity only for an --rpc-only override", () => { + const base = testnetBradbury as any; + expect(base.genesisHash).toBeTruthy(); + expect(base.addressManager).toBeTruthy(); + + const providerOnly = resolveNetwork("bradbury-provider", { + "bradbury-provider": {base: "testnet-bradbury", overrides: {rpcUrl: "https://provider.example"}}, + }) as any; + expect(providerOnly.genesisHash).toBe(base.genesisHash); + expect(providerOnly.addressManager).toBe(base.addressManager); + + for (const overrides of [{staking: ADDR_1}, {chainId: 4222}]) { + const other = resolveNetwork("other-deployment", { + "other-deployment": {base: "testnet-bradbury", overrides: overrides as any}, + }) as any; + expect(other).not.toHaveProperty("genesisHash"); + expect(other).not.toHaveProperty("addressManager"); + } + // The base chain object is never mutated. + expect(base.genesisHash).toBeTruthy(); + expect(base.addressManager).toBeTruthy(); + }); + test("StakingAction.getNetwork accepts a custom alias", () => { const stakingAction = new StakingAction(); (vi.spyOn(stakingAction as any, "getConfigByKey") as any).mockImplementation((key: string) => { From 598874cd4d6fee81a8c9d7ffb20856203220627c Mon Sep 17 00:00:00 2001 From: Darien Hernandez Date: Mon, 19 Oct 2026 23:37:41 +0200 Subject: [PATCH 5/8] fix(staking): point the epoch-0 note at validator-info Name the concrete command to check activation and document the satisfied-minimum branch in assertOrWarnSelfStakeMinimum. Co-Authored-By: Claude Opus 5.5 --- src/lib/actions/BaseAction.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/lib/actions/BaseAction.ts b/src/lib/actions/BaseAction.ts index 0b7b653b..55ebb5da 100644 --- a/src/lib/actions/BaseAction.ts +++ b/src/lib/actions/BaseAction.ts @@ -596,7 +596,9 @@ export class BaseAction extends ConfigFileManager { * surfaced. * * Behaviour: - * - epoch 0: informational note only, never blocks. + * - epoch 0: informational note only, never blocks. When the resulting + * self-stake already meets the minimum, the note says so without implying + * activation (the join only registers the validator). * - resulting self-stake >= min: silent (already eligible). * - resulting self-stake < min: block by THROWING (the caller's catch turns * it into a failSpinner) unless `force` is set, in which case it warns and @@ -623,7 +625,7 @@ export class BaseAction extends ConfigFileManager { if (resultingSelfStakeRaw >= minStakeRaw) { this.logInfo( "Epoch 0: self-stake meets the current minimum. Registration does not imply activation; " + - "check the validator activation epoch and priming status.", + "check it with 'genlayer staking validator-info '.", ); return; } From 5e11698fe6b3684f595bd0ad01103a01913114f5 Mon Sep 17 00:00:00 2001 From: Darien Hernandez Date: Mon, 19 Oct 2026 23:37:41 +0200 Subject: [PATCH 6/8] revert: move GENLAYER_CONFIG_DIR out of the Phase 2 PR Unrelated to Bradbury Phase 2 qualification and not used by docs, skills, node or e2e. It also isn't fully isolated: unlocked keys are cached in the OS keychain by account name only. Recoverable from da816a8 for a separate PR. Co-Authored-By: Claude Opus 5.5 --- README.md | 4 ---- src/lib/config/ConfigFileManager.ts | 2 +- tests/libs/configFileManager.test.ts | 10 ---------- 3 files changed, 1 insertion(+), 15 deletions(-) diff --git a/README.md b/README.md index 9700b6c2..2658970e 100644 --- a/README.md +++ b/README.md @@ -718,7 +718,3 @@ We welcome contributions to GenLayerJS SDK! Whether it's new features, improved ## License This project is licensed under the ... License - see the [LICENSE](LICENSE) file for details. - -### Isolated validator configuration - -Set `GENLAYER_CONFIG_DIR` to an absolute directory to keep a validator qualification account and network configuration separate from your normal `~/.genlayer` configuration. The directory and its keystore subdirectory are created with mode 0700. diff --git a/src/lib/config/ConfigFileManager.ts b/src/lib/config/ConfigFileManager.ts index 659aea81..48e7564e 100644 --- a/src/lib/config/ConfigFileManager.ts +++ b/src/lib/config/ConfigFileManager.ts @@ -13,7 +13,7 @@ export class ConfigFileManager { private configFilePath: string; private keystoresPath: string; - constructor(baseFolder: string = process.env.GENLAYER_CONFIG_DIR || ".genlayer/", configFileName: string = "genlayer-config.json") { + constructor(baseFolder: string = ".genlayer/", configFileName: string = "genlayer-config.json") { this.folderPath = path.resolve(os.homedir(), baseFolder); this.configFilePath = path.resolve(this.folderPath, configFileName); this.keystoresPath = path.resolve(this.folderPath, "keystores"); diff --git a/tests/libs/configFileManager.test.ts b/tests/libs/configFileManager.test.ts index d1eddf6b..e92c5d47 100644 --- a/tests/libs/configFileManager.test.ts +++ b/tests/libs/configFileManager.test.ts @@ -26,7 +26,6 @@ describe("ConfigFileManager", () => { afterEach(() => { vi.restoreAllMocks(); - vi.unstubAllEnvs(); }); test("ensures folder and config file are created if they don't exist", () => { @@ -58,15 +57,6 @@ describe("ConfigFileManager", () => { expect(fs.mkdirSync).not.toHaveBeenCalled(); }); - test("isolates qualification accounts using GENLAYER_CONFIG_DIR", () => { - const isolatedPath = path.resolve(mockHome, "qualification", "config"); - vi.stubEnv("GENLAYER_CONFIG_DIR", isolatedPath); - const isolated = new ConfigFileManager(); - expect(isolated.getFolderPath()).toBe(isolatedPath); - expect(isolated.getKeystorePath("operator")).toBe(path.join(isolatedPath, "keystores", "operator.json")); - expect(new ConfigFileManager("explicit").getFolderPath()).toBe(path.resolve(mockHome, "explicit")); - }); - test("getFolderPath returns the correct folder path", () => { expect(configFileManager.getFolderPath()).toBe(mockFolderPath); }); From c69f24fc92b65529a5c0c1ee3ebc895f699e4793 Mon Sep 17 00:00:00 2001 From: Darien Hernandez Date: Tue, 20 Oct 2026 00:14:48 +0200 Subject: [PATCH 7/8] revert(network): drop custom-profile identity handling genlayer-js#236 removed the transport identity guard (genesisHash/addressManager) before merging, so custom profiles have nothing to drop. Reverts e93b0e5. Co-Authored-By: Claude Opus 5.5 --- src/lib/networks/customNetworks.ts | 12 ----------- tests/actions/customNetworkProfiles.test.ts | 23 --------------------- 2 files changed, 35 deletions(-) diff --git a/src/lib/networks/customNetworks.ts b/src/lib/networks/customNetworks.ts index 602b1e4c..d8c624b3 100644 --- a/src/lib/networks/customNetworks.ts +++ b/src/lib/networks/customNetworks.ts @@ -196,18 +196,6 @@ export function applyCustomNetworkProfile( delete (chain as any).blockExplorers; } - // The base chain's deployment identity (genesis hash + AddressManager) is - // checked by the SDK before every request. Like the explorer, it describes the - // BASE deployment: a profile that overrides contracts or the chain ID targets a - // different one, so an inherited identity would reject every call. Keep it only - // for an --rpc-only profile (another provider for the same deployment). - const targetsOtherDeployment = - overrides.chainId !== undefined || CONTRACT_OVERRIDES.some(contract => overrides[contract.overrideKey]); - if (targetsOtherDeployment) { - delete (chain as any).genesisHash; - delete (chain as any).addressManager; - } - return chain; } diff --git a/tests/actions/customNetworkProfiles.test.ts b/tests/actions/customNetworkProfiles.test.ts index 6fd33057..e218ca27 100644 --- a/tests/actions/customNetworkProfiles.test.ts +++ b/tests/actions/customNetworkProfiles.test.ts @@ -323,29 +323,6 @@ describe("custom network profiles", () => { expect(resolvedChain.name).not.toBe(baseChain.name); }); - test("custom profile keeps the base deployment identity only for an --rpc-only override", () => { - const base = testnetBradbury as any; - expect(base.genesisHash).toBeTruthy(); - expect(base.addressManager).toBeTruthy(); - - const providerOnly = resolveNetwork("bradbury-provider", { - "bradbury-provider": {base: "testnet-bradbury", overrides: {rpcUrl: "https://provider.example"}}, - }) as any; - expect(providerOnly.genesisHash).toBe(base.genesisHash); - expect(providerOnly.addressManager).toBe(base.addressManager); - - for (const overrides of [{staking: ADDR_1}, {chainId: 4222}]) { - const other = resolveNetwork("other-deployment", { - "other-deployment": {base: "testnet-bradbury", overrides: overrides as any}, - }) as any; - expect(other).not.toHaveProperty("genesisHash"); - expect(other).not.toHaveProperty("addressManager"); - } - // The base chain object is never mutated. - expect(base.genesisHash).toBeTruthy(); - expect(base.addressManager).toBeTruthy(); - }); - test("StakingAction.getNetwork accepts a custom alias", () => { const stakingAction = new StakingAction(); (vi.spyOn(stakingAction as any, "getConfigByKey") as any).mockImplementation((key: string) => { From 0b1a29a6b0cfb271df76dca0ca6815cf679cf7aa Mon Sep 17 00:00:00 2001 From: Darien Hernandez Date: Tue, 20 Oct 2026 00:56:35 +0200 Subject: [PATCH 8/8] fix(deps): pin genlayer-js to the merged Bradbury Phase 2 SDK Pin the merge commit of genlayerlabs/genlayer-js#236 on v2-dev (b629021, tree identical to its reviewed head 7ce4330), matching how earlier pins track v2-dev merge commits. Restores the lockfile integrity hash. Co-Authored-By: Claude Opus 5.5 --- package-lock.json | 5 +++-- package.json | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5771e52a..d55272d0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,7 +46,7 @@ "eslint-config-prettier": "^10.0.0", "eslint-import-resolver-typescript": "^4.0.0", "eslint-plugin-import": "^2.29.1", - "genlayer-js": "github:genlayerlabs/genlayer-js#7e29c039aba5febcd0965d98c991c793bf975ff8", + "genlayer-js": "github:genlayerlabs/genlayer-js#b629021092e23db08fd884f982d8eb34682f7bad", "jsdom": "^26.0.0", "prettier": "^3.2.5", "release-it": "^19.0.0", @@ -5683,7 +5683,8 @@ }, "node_modules/genlayer-js": { "version": "2.0.0-rc.1", - "resolved": "git+ssh://git@github.com/genlayerlabs/genlayer-js.git#7e29c039aba5febcd0965d98c991c793bf975ff8", + "resolved": "git+ssh://git@github.com/genlayerlabs/genlayer-js.git#b629021092e23db08fd884f982d8eb34682f7bad", + "integrity": "sha512-AH0HmJDHwWz0ErVoT4hkvcD4Ya1QNRestL2Qz7vnJaAc7wxAQXS49z0w85YOjaYrDN+U++u5O8yjHJZ0Az8NMA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index dd578434..ba24f1b9 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "eslint-config-prettier": "^10.0.0", "eslint-import-resolver-typescript": "^4.0.0", "eslint-plugin-import": "^2.29.1", - "genlayer-js": "github:genlayerlabs/genlayer-js#7e29c039aba5febcd0965d98c991c793bf975ff8", + "genlayer-js": "github:genlayerlabs/genlayer-js#b629021092e23db08fd884f982d8eb34682f7bad", "jsdom": "^26.0.0", "prettier": "^3.2.5", "release-it": "^19.0.0",