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
32 changes: 3 additions & 29 deletions .github/workflows/publish.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,8 +9,8 @@ name: Publish Package to NPM
# Release. It never bumps or tags by itself.
#
# Stable tags such as v0.39.2 publish to npm's latest dist-tag.
# Prerelease tags such as v0.40.0-clarke.1 publish to a matching
# channel dist-tag (clarke) and create a GitHub prerelease.
# Prerelease tags such as v0.40.0-rc.3 publish to a matching
# channel dist-tag (rc) and create a GitHub prerelease.
# Prereleases must never become latest.
on:
workflow_dispatch:
Expand DownExpand Up@@ -47,35 +47,9 @@ jobs:

TAG_VERSION="${GITHUB_REF_NAME#v}"
PKG_VERSION="$(node -p "require('./package.json').version")"
if [ "$TAG_VERSION" != "$PKG_VERSION" ]; then
echo "Tag ($TAG_VERSION) and package.json ($PKG_VERSION) disagree — refusing to publish." >&2
echo "Re-cut the release via scripts/release.sh so the tag and the committed version match." >&2
exit 1
fi

IS_PRERELEASE=false
NPM_DIST_TAG=latest
if [[ "$TAG_VERSION" == *-* ]]; then
IS_PRERELEASE=true
PRERELEASE_SUFFIX="${TAG_VERSION#*-}"
PRERELEASE_SUFFIX="${PRERELEASE_SUFFIX%%+*}"
NPM_DIST_TAG="${PRERELEASE_SUFFIX%%.*}"

if [ -z "$NPM_DIST_TAG" ] || [ "$NPM_DIST_TAG" = "latest" ]; then
echo "Invalid prerelease npm dist-tag: '$NPM_DIST_TAG'" >&2
exit 1
fi

if [[ "$NPM_DIST_TAG" =~ ^v?[0-9] ]]; then
echo "Invalid prerelease npm dist-tag '$NPM_DIST_TAG': dist-tags must not look like versions." >&2
exit 1
fi
fi
node scripts/resolve-release-channel.mjs "$TAG_VERSION" "$PKG_VERSION" >> "$GITHUB_OUTPUT"

echo "Tag $GITHUB_REF_NAME matches package.json $PKG_VERSION."
echo "npm dist-tag: $NPM_DIST_TAG"
echo "is_prerelease=$IS_PRERELEASE" >> "$GITHUB_OUTPUT"
echo "npm_dist_tag=$NPM_DIST_TAG" >> "$GITHUB_OUTPUT"

- run: npm run build

Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -166,6 +166,7 @@ OPTIONS (add):

EXAMPLES:
genlayer network set
genlayer network set studio-dev
genlayer network set testnet-bradbury
genlayer network info
genlayer network list
Expand All@@ -178,6 +179,8 @@ EXAMPLES:

Custom networks are stored by alias and can be selected anywhere a built-in
network can, via `--network <alias>` (or `genlayer network set <alias>`).
The built-in `studio-dev` preview points to `https://studio-dev.genlayer.com/api`
on chain ID `61997`; stable `studionet` remains unchanged.

#### Deploy and Call Intelligent Contracts

Expand Down
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -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#8f72796efa6f1f52d420957bd253771c8583ca14",
"genlayer-js": "github:genlayerlabs/genlayer-js#facd9e9dc9a289d0110fe3b5b1a14a2938fe6e01",
"jsdom": "^26.0.0",
"prettier": "^3.2.5",
"release-it": "^19.0.0",
Expand Down
53 changes: 53 additions & 0 deletions scripts/resolve-release-channel.mjs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
#!/usr/bin/env node

import {fileURLToPath} from "node:url";
import {resolve} from "node:path";

const DIST_TAG_PATTERN = /^[a-z][a-z0-9._-]*$/i;

export function resolveReleaseChannel(tagVersion, packageVersion) {
if (!tagVersion || !packageVersion) {
throw new Error("Usage: resolve-release-channel.mjs <tag-version> <package-version>");
}
if (tagVersion !== packageVersion) {
throw new Error(
`Tag (${tagVersion}) and package.json (${packageVersion}) disagree — refusing to publish.`,
);
}

const prerelease = tagVersion.includes("-");
if (!prerelease) {
return {isPrerelease: false, npmDistTag: "latest"};
}

const suffix = tagVersion.slice(tagVersion.indexOf("-") + 1).split("+", 1)[0];
const npmDistTag = suffix.split(".", 1)[0];
if (
!npmDistTag ||
npmDistTag === "latest" ||
!DIST_TAG_PATTERN.test(npmDistTag) ||
/^v?[0-9]/i.test(npmDistTag)
) {
throw new Error(`Invalid prerelease npm dist-tag: '${npmDistTag}'`);
}

return {isPrerelease: true, npmDistTag};
}

function main() {
try {
const {isPrerelease, npmDistTag} = resolveReleaseChannel(
process.argv[2],
process.argv[3],
);
process.stdout.write(`is_prerelease=${isPrerelease}\n`);
process.stdout.write(`npm_dist_tag=${npmDistTag}\n`);
} catch (error) {
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
process.exitCode = 1;
}
}

if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
main();
}
4 changes: 2 additions & 2 deletions src/commands/staking/wizard.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -329,8 +329,8 @@ export class ValidatorWizardAction extends StakingAction {
}

const currentNetwork = this.getConfigByKey("network");
// Exclude studionet - not compatible with staking
const excludedNetworks = ["studionet"];
// Studio deployments are not compatible with staking.
const excludedNetworks = ["studionet", "studio-dev"];
const networks = Object.entries(BUILT_IN_NETWORKS)
.filter(([alias]) => !excludedNetworks.includes(alias))
.map(([alias, chain]) => ({
Expand Down
9 changes: 8 additions & 1 deletion src/lib/actions/BaseAction.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,13 @@ import chalk from "chalk";
import inquirer from "inquirer";
import { inspect } from "util";
import {createClient, createAccount} from "genlayer-js";
import {localnet, studionet, testnetAsimov, testnetBradbury} from "genlayer-js/chains";
import {
localnet,
studioDevnet,
studionet,
testnetAsimov,
testnetBradbury,
} from "genlayer-js/chains";
import type {GenLayerClient, GenLayerChain, Hash, Address, Account} from "genlayer-js/types";
import {
applyCustomNetworkProfile,
Expand All@@ -23,6 +29,7 @@ import {JsonRpcClient} from "../clients/jsonRpcClient";
export const BUILT_IN_NETWORKS: Record<string, GenLayerChain> = {
"localnet": localnet,
"studionet": studionet,
"studio-dev": studioDevnet,
"testnet-asimov": testnetAsimov,
"testnet-bradbury": testnetBradbury,
};
Expand Down
19 changes: 17 additions & 2 deletions tests/actions/customNetworkProfiles.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,7 +6,7 @@ import {tmpdir} from "os";
import {NetworkActions} from "../../src/commands/network/setNetwork";
import {resolveNetwork} from "../../src/lib/actions/BaseAction";
import {parseDeploymentObject} from "../../src/lib/networks/customNetworks";
import {testnetBradbury} from "genlayer-js/chains";
import {studioDevnet, studionet, testnetBradbury} from "genlayer-js/chains";
import {StakingAction} from "../../src/commands/staking/StakingAction";

const ADDR_1 = "0x1111111111111111111111111111111111111111";
Expand DownExpand Up@@ -42,6 +42,21 @@ describe("custom network profiles", () => {
fs.rmSync(tempHome, {recursive: true, force: true});
});

test("studio-dev is a dedicated built-in without moving stable studionet", () => {
const preview = resolveNetwork("studio-dev");
const stable = resolveNetwork("studionet");

expect(preview).toBe(studioDevnet);
expect(preview.id).toBe(61_997);
expect(preview.rpcUrls.default.http).toEqual(["https://studio-dev.genlayer.com/api"]);
expect(preview.consensusMainContract?.address).toBe(studionet.consensusMainContract?.address);
expect(preview.consensusDataContract?.address).toBe(studionet.consensusDataContract?.address);

expect(stable).toBe(studionet);
expect(stable.id).toBe(61_999);
expect(stable.rpcUrls.default.http).toEqual(["https://studio.genlayer.com/api"]);
});

test("network add stores flags-only overrides", async () => {
await action.addNetwork("bradbury-clarke", {
base: "testnet-bradbury",
Expand DownExpand Up@@ -177,7 +192,7 @@ describe("custom network profiles", () => {
expect(failSpy).toHaveBeenNthCalledWith(
2,
"Failed to add custom network profile",
"Base network must be one of: localnet, studionet, testnet-asimov, testnet-bradbury",
"Base network must be one of: localnet, studionet, studio-dev, testnet-asimov, testnet-bradbury",
);
expect(failSpy).toHaveBeenNthCalledWith(
3,
Expand Down
18 changes: 17 additions & 1 deletion tests/actions/setNetwork.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,13 @@ import {describe, test, vi, beforeEach, afterEach, expect} from "vitest";
import {NetworkActions} from "../../src/commands/network/setNetwork";
import {ConfigFileManager} from "../../src/lib/config/ConfigFileManager";
import inquirer from "inquirer";
import {localnet, studionet, testnetAsimov, testnetBradbury} from "genlayer-js/chains";
import {
localnet,
studioDevnet,
studionet,
testnetAsimov,
testnetBradbury,
} from "genlayer-js/chains";

vi.mock("../../src/lib/config/ConfigFileManager");
vi.mock("inquirer");
Expand DownExpand Up@@ -59,6 +65,15 @@ describe("NetworkActions", () => {
);
});

test("setNetwork method sets studio-dev by alias", async () => {
await networkActions.setNetwork("studio-dev");

expect(networkActions["writeConfig"]).toHaveBeenCalledWith("network", "studio-dev");
expect(networkActions["succeedSpinner"]).toHaveBeenCalledWith(
`Network successfully set to ${studioDevnet.name}`,
);
});

test("setNetwork method sets testnet-asimov by name", async () => {
await networkActions.setNetwork(testnetAsimov.name);

Expand DownExpand Up@@ -108,6 +123,7 @@ describe("NetworkActions", () => {
choices: [
{name: localnet.name, value: "localnet"},
{name: studionet.name, value: "studionet"},
{name: studioDevnet.name, value: "studio-dev"},
{name: testnetAsimov.name, value: "testnet-asimov"},
{name: testnetBradbury.name, value: "testnet-bradbury"},
],
Expand Down
1 change: 1 addition & 0 deletions tests/actions/staking.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,6 +35,7 @@ vi.mock("genlayer-js", () => ({
vi.mock("genlayer-js/chains", () => ({
localnet: {id: 1, name: "localnet", rpcUrls: {default: {http: ["http://localhost:8545"]}}},
studionet: {id: 2, name: "studionet", rpcUrls: {default: {http: ["https://studionet.genlayer.com"]}}},
studioDevnet: {id: 61997, name: "studio-dev", rpcUrls: {default: {http: ["https://studio-dev.genlayer.com/api"]}}},
testnetAsimov: {
id: 3,
name: "testnet-asimov",
Expand Down
25 changes: 25 additions & 0 deletions tests/actions/stakingWizard.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -64,6 +64,31 @@ function mockRegistrationHelpers(action: ValidatorWizardAction) {
vi.spyOn(action as any, "createVestingValidatorRegistration").mockResolvedValue(mockRegistration);
}

describe("ValidatorWizardAction Studio network filtering", () => {
afterEach(() => {
vi.restoreAllMocks();
});

test("does not offer stable or preview Studio deployments for staking", async () => {
const action = new ValidatorWizardAction();
vi.spyOn(action as any, "getConfigByKey").mockReturnValue("studio-dev");
vi.spyOn(action as any, "getCustomNetworks").mockReturnValue({});
vi.spyOn(action as any, "writeConfig").mockImplementation(() => {});
vi.mocked(inquirer.prompt).mockResolvedValueOnce({
selectedNetwork: "testnet-asimov",
});

await (action as any).stepNetworkSelection({}, {});

const questions = vi.mocked(inquirer.prompt).mock.calls[0][0] as any[];
const aliases = questions[0].choices.map((choice: {value: string}) => choice.value);
expect(aliases).not.toContain("studionet");
expect(aliases).not.toContain("studio-dev");
expect(aliases).toContain("testnet-asimov");
expect(aliases).toContain("testnet-bradbury");
});
});

describe("ValidatorWizardAction --wallet browser (owner)", () => {
let action: ValidatorWizardAction;
let sendTransaction: ReturnType<typeof vi.fn>;
Expand Down
1 change: 1 addition & 0 deletions tests/actions/vesting.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,6 +25,7 @@ vi.mock("genlayer-js", () => ({
vi.mock("genlayer-js/chains", () => ({
localnet: {id: 1, name: "localnet", rpcUrls: {default: {http: ["http://localhost:8545"]}}},
studionet: {id: 2, name: "studionet", rpcUrls: {default: {http: ["https://studionet.genlayer.com"]}}},
studioDevnet: {id: 61997, name: "studio-dev", rpcUrls: {default: {http: ["https://studio-dev.genlayer.com/api"]}}},
testnetAsimov: {
id: 3,
name: "testnet-asimov",
Expand Down
1 change: 1 addition & 0 deletions tests/commands/balances.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,6 +14,7 @@ vi.mock("genlayer-js", () => ({
vi.mock("genlayer-js/chains", () => ({
localnet: {id: 1, name: "localnet", rpcUrls: {default: {http: ["http://localhost:8545"]}}, consensusMainContract: {address: "0x00000000000000000000000000000000000000c0"}},
studionet: {id: 2, name: "studionet", rpcUrls: {default: {http: ["https://studio.genlayer.com"]}}, consensusMainContract: {address: "0x00000000000000000000000000000000000000c0"}},
studioDevnet: {id: 61997, name: "studio-dev", rpcUrls: {default: {http: ["https://studio-dev.genlayer.com/api"]}}, consensusMainContract: {address: "0x00000000000000000000000000000000000000c0"}},
testnetAsimov: {id: 3, name: "testnet-asimov", rpcUrls: {default: {http: ["https://rpc-asimov.genlayer.com"]}}, consensusMainContract: {address: "0x00000000000000000000000000000000000000c0"}},
testnetBradbury: {id: 4, name: "testnet-bradbury", rpcUrls: {default: {http: ["https://rpc-bradbury.genlayer.com"]}}, consensusMainContract: {address: "0x00000000000000000000000000000000000000c0"}},
}));
Expand Down
1 change: 1 addition & 0 deletions tests/commands/vesting.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -22,6 +22,7 @@ vi.mock("genlayer-js", () => ({
vi.mock("genlayer-js/chains", () => ({
localnet: {id: 1, name: "localnet", rpcUrls: {default: {http: ["http://localhost:8545"]}}},
studionet: {id: 2, name: "studionet", rpcUrls: {default: {http: ["https://studio.genlayer.com"]}}},
studioDevnet: {id: 61997, name: "studio-dev", rpcUrls: {default: {http: ["https://studio-dev.genlayer.com/api"]}}},
testnetAsimov: {id: 3, name: "testnet-asimov", rpcUrls: {default: {http: ["https://testnet.genlayer.com"]}}},
testnetBradbury: {id: 4, name: "testnet-bradbury", rpcUrls: {default: {http: ["https://testnet.genlayer.com"]}}},
}));
Expand Down
36 changes: 36 additions & 0 deletions tests/release-channel.test.mjs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
import {describe, expect, it} from "vitest";
import {resolveReleaseChannel} from "../scripts/resolve-release-channel.mjs";

Check failure on line 2 in tests/release-channel.test.mjs

View workflow job for this annotation

GitHub Actions/ Build and Test (windows-latest)

tests/release-channel.test.mjs

SyntaxError: Invalid or unexpected token ❯ tests/release-channel.test.mjs:2:1

describe("release channel policy", () => {
it("publishes stable releases to latest", () => {
expect(resolveReleaseChannel("0.40.0", "0.40.0")).toEqual({
isPrerelease: false,
npmDistTag: "latest",
});
});

it("publishes v0.40.0-rc.3 to rc without moving latest", () => {
expect(resolveReleaseChannel("0.40.0-rc.3", "0.40.0-rc.3")).toEqual({
isPrerelease: true,
npmDistTag: "rc",
});
});

it("rejects mismatched tags", () => {
expect(() => resolveReleaseChannel("0.40.0-rc.3", "0.40.0-clarke.4")).toThrow(
"disagree",
);
});

it("rejects prereleases that could target latest", () => {
expect(() => resolveReleaseChannel("0.40.0-latest.1", "0.40.0-latest.1")).toThrow(
"Invalid prerelease npm dist-tag",
);
});

it("rejects version-like prerelease channels", () => {
expect(() => resolveReleaseChannel("0.40.0-2.1", "0.40.0-2.1")).toThrow(
"Invalid prerelease npm dist-tag",
);
});
});
Loading