From 59b9e9afa6904898e5dab621f2888683747181db Mon Sep 17 00:00:00 2001 From: Talles Borges Date: Thu, 18 Jun 2026 04:23:46 +0200 Subject: [PATCH] feat(config): resolve active env at runtime from PLAYGROUND_ENV getChainConfig/getNetworkLabel/getTokenSymbol/getPgasAssetId now default to a new getActiveEnv() that reads PLAYGROUND_ENV (validated against CONFIGS) and falls back to DEFAULT_ENV. This lets the direct-chain layer (getConnection, pairing, registry, drip) follow the selected network, so one build can target any wired env (Summit/Paseo) at runtime without flipping ACTIVE_TESTNET_ENV. Unset PLAYGROUND_ENV => unchanged behaviour. --- .changeset/playground-env-runtime-override.md | 13 +++++ CLAUDE.md | 2 +- src/config.test.ts | 49 ++++++++++++++++++- src/config.ts | 25 ++++++++-- 4 files changed, 81 insertions(+), 8 deletions(-) create mode 100644 .changeset/playground-env-runtime-override.md diff --git a/.changeset/playground-env-runtime-override.md b/.changeset/playground-env-runtime-override.md new file mode 100644 index 00000000..b1e62400 --- /dev/null +++ b/.changeset/playground-env-runtime-override.md @@ -0,0 +1,13 @@ +--- +"playground-cli": minor +--- + +Resolve the active environment at runtime from a `PLAYGROUND_ENV` env var. + +`getChainConfig()`, `getNetworkLabel()`, `getTokenSymbol()`, and `getPgasAssetId()` now +default to a new `getActiveEnv()` helper, which reads `PLAYGROUND_ENV` (validated against +the wired `CONFIGS`) and falls back to the build-time `DEFAULT_ENV` when it is unset or +unknown. This makes the direct-chain layer (`getConnection`, pairing, registry, drip) +follow the selected network, so a single build can target any wired env (e.g. Summit or +Paseo) at runtime without flipping `ACTIVE_TESTNET_ENV` and rebuilding. Existing callers +are unaffected: with `PLAYGROUND_ENV` unset, behaviour is identical to before. diff --git a/CLAUDE.md b/CLAUDE.md index 14c9e498..0576e451 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -32,7 +32,7 @@ These aren't self-evident from reading the code and have bitten us before. Treat ### Network / env -- **`ACTIVE_TESTNET_ENV` (`src/config.ts`) is THE network switch — flipping it is the entire "change networks" PR.** It feeds both `DEFAULT_ENV` and the legacy `testnet` alias in `resolveLegacyEnv`, so one constant moves the whole CLI. `paseo-next-v2` and `summit` are wired in `CONFIGS` today; other envs throw "not supported" from `getChainConfig()`. The deploy `--env` flag accepts the new ids plus the legacy `testnet|mainnet` aliases. NOTE: the direct-chain layer (`getConnection()`) is hardwired to `getChainConfig()` (DEFAULT_ENV) and the `paseo_*` descriptors — `--env` only reroutes what we hand polkadot-app-deploy's `deploy()`, not our own reads. When adding an env, populate `CONFIGS` (every field, including `cdmEnvName` and `tokenSymbol`) and verify descriptors exist in `@parity/product-sdk-descriptors`. The `paseo-*` descriptor exports we use today are generated against paseo-next-v2 endpoints despite the unversioned names. Each env also carries a display-only `tokenSymbol` (paseo-next-v2 → `PAS`, summit → `SUM`); read it via `getTokenSymbol()` (the sole consumer is `formatPas` in `src/utils/account/drip.ts`), so flipping `ACTIVE_TESTNET_ENV` relabels every balance/drip amount automatically. It is NOT validated by the `config.test.ts` divergence guard — environments.json carries no symbol — so set it from the chain's own token, not upstream. +- **`ACTIVE_TESTNET_ENV` (`src/config.ts`) is THE network switch — flipping it is the entire "change networks" PR.** It feeds both `DEFAULT_ENV` and the legacy `testnet` alias in `resolveLegacyEnv`, so one constant moves the whole CLI. `paseo-next-v2` and `summit` are wired in `CONFIGS` today; other envs throw "not supported" from `getChainConfig()`. The deploy `--env` flag accepts the new ids plus the legacy `testnet|mainnet` aliases. NOTE: the direct-chain layer (`getConnection()`, pairing, registry, drip) resolves its env from `getActiveEnv()` — the `PLAYGROUND_ENV` var when set and wired in `CONFIGS`, else `DEFAULT_ENV` — and selects descriptors per that env, so a single build can point its OWN reads at a different wired network at runtime without flipping `ACTIVE_TESTNET_ENV`. The `--env` flag still only reroutes what we hand polkadot-app-deploy's `deploy()`; `PLAYGROUND_ENV` is the lever for our own reads. When adding an env, populate `CONFIGS` (every field, including `cdmEnvName` and `tokenSymbol`) and verify descriptors exist in `@parity/product-sdk-descriptors`. The `paseo-*` descriptor exports we use today are generated against paseo-next-v2 endpoints despite the unversioned names. Each env also carries a display-only `tokenSymbol` (paseo-next-v2 → `PAS`, summit → `SUM`); read it via `getTokenSymbol()` (the sole consumer is `formatPas` in `src/utils/account/drip.ts`), so flipping `ACTIVE_TESTNET_ENV` relabels every balance/drip amount automatically. It is NOT validated by the `config.test.ts` divergence guard — environments.json carries no symbol — so set it from the chain's own token, not upstream. - **All chain URLs / contract addresses live in `src/config.ts` — EXCEPT the CDM meta-registry address, which lives in `@parity/cdm-env` and resolves per-env via `getRegistryAddress(cfg.cdmEnvName)`.** Never inline a websocket URL or `0x…` address anywhere else. `config.ts` carries a `cdmEnvName` per env (the name cdm-env keys on — our `summit` is cdm-env's `w3s`, paseo passes through as `paseo-next-v2`); `src/utils/registry.ts` and `src/commands/contract.ts` resolve the meta-registry root from it and inject it over `cdm.json::registry` (which is just whatever `cdm i` baked — do NOT hand-edit `cdm.json`). `getRegistryAddress` returns `""` for an unknown/undeployed env, and `registry.ts` throws a clear "bump @parity/cdm-env" error on empty. - **Adding a network / Summit — the deployment-doc checklist.** A divergence guard (`src/config.test.ts`) reads polkadot-app-deploy's bundled `environments.json` via its public `loadEnvironments()` and asserts every wired `CONFIGS` env's endpoints/network/gateway match upstream, AND that the default env's `getRegistryAddress(cdmEnvName)` is non-empty — so the one-line switch can't merge until the target is genuinely ready. To point a release at Summit: (1) confirm `@parity/cdm-env` ships a non-empty `w3s` registry address — SATISFIED as of `cdm-env@2.0.6` (`0xa5747e60ae27f93e92019e4021abfc4957050141`; was `""` through 2.0.5). Re-verify with `node --input-type=module -e "import { getRegistryAddress } from '@parity/cdm-env'; console.log(getRegistryAddress('w3s'))"` (cdm-env is ESM-only — the `require()` form throws `ERR_PACKAGE_PATH_NOT_EXPORTED`) and bump the dep if a future env regresses it; (2) confirm Summit endpoints still match polkadot-app-deploy's `assets/environments.json` `summit`/`chains[*].endpoints.summit` (the guard enforces this — bump `@parity/polkadot-app-deploy` and update the `SUMMIT` block together if they drift); (3) flip `ACTIVE_TESTNET_ENV` to `"summit"`; (4) `pnpm typecheck && pnpm test` (the guard is the gate). Prerequisites that land upstream BEFORE the CLI switch (not our PR): the Summit CDM meta-registry contract + the playground-registry published to CDM (resolves by name `@w3s/playground-registry`), the Summit DotNS contracts (owned by polkadot-app-deploy's env catalog, keyed by env id), and the `w3s` registry address in `@parity/cdm-env`. Heads-up: cdm-env's `w3s.ipfsGatewayUrl` is `""` and disagrees with environments.json's `summit.ipfs` — we source the gateway from environments.json on purpose, so the guard checks against that. - **Upstream SDK status for Summit (audited June 2026 at the pinned versions — no bumps needed).** `@parity/product-sdk` already ships Summit at the versions we use: `descriptors@0.6.0` exports `summit-asset-hub`/`summit-bulletin`/`summit-individuality`, and `cloud-storage@0.6.0` carries the `summit` network preset (genesis + bulletin). product-sdk calls the network **`summit`**; only `@parity/cdm-env` and this CLI use the key **`w3s`** (that two-name split is why `cdmEnvName` exists — don't "fix" it). The CLI does NOT depend on `@parity/product-sdk-chain-client`, so its preset table is irrelevant here. `@novasamatech/*` (host-papp/statement-store/host-api/sdk-statement, via `@parity/product-sdk-terminal@0.5.0` → host-papp 0.8.7) is fully **env-agnostic**: SSO handshake, statement-store topic/SessionId derivation, and allowance slot-key derivation embed no network constant (genesisHash is a runtime SCALE field, not a baked value), and host-papp's hardcoded `SS_*_ENDPOINTS` People defaults are dead fallbacks because the terminal always forwards `getChainConfig().peopleEndpoints`. Net: the ONLY thing still pending for a Summit switch is `@parity/cdm-env` shipping the non-empty `w3s` registry address — nothing in product-sdk or triangle-js-sdks. diff --git a/src/config.test.ts b/src/config.test.ts index a36d9c0e..2383673d 100644 --- a/src/config.test.ts +++ b/src/config.test.ts @@ -34,10 +34,20 @@ * be non-empty, so nobody can ship a default whose registry isn't deployed yet. */ -import { describe, expect, it } from "vitest"; +import { afterEach, describe, expect, it } from "vitest"; import { loadEnvironments } from "@parity/polkadot-app-deploy"; import { getRegistryAddress } from "@parity/cdm-env"; -import { CONFIGS, DEFAULT_ENV, getPgasAssetId, type ChainConfig, type Env } from "./config.js"; +import { + CONFIGS, + DEFAULT_ENV, + getActiveEnv, + getChainConfig, + getNetworkLabel, + getPgasAssetId, + getTokenSymbol, + type ChainConfig, + type Env, +} from "./config.js"; const { doc } = await loadEnvironments(); @@ -52,6 +62,41 @@ describe("getPgasAssetId", () => { }); }); +describe("getActiveEnv (PLAYGROUND_ENV runtime override)", () => { + const original = process.env.PLAYGROUND_ENV; + afterEach(() => { + if (original === undefined) delete process.env.PLAYGROUND_ENV; + else process.env.PLAYGROUND_ENV = original; + }); + + it("falls back to DEFAULT_ENV when PLAYGROUND_ENV is unset", () => { + delete process.env.PLAYGROUND_ENV; + expect(getActiveEnv()).toBe(DEFAULT_ENV); + }); + + it("uses a wired PLAYGROUND_ENV value", () => { + process.env.PLAYGROUND_ENV = "summit"; + expect(getActiveEnv()).toBe("summit"); + }); + + it("trims surrounding whitespace", () => { + process.env.PLAYGROUND_ENV = " summit "; + expect(getActiveEnv()).toBe("summit"); + }); + + it("falls back to DEFAULT_ENV for an unwired/garbage value", () => { + process.env.PLAYGROUND_ENV = "not-a-real-env"; + expect(getActiveEnv()).toBe(DEFAULT_ENV); + }); + + it("threads through the no-arg config + display helpers", () => { + process.env.PLAYGROUND_ENV = "summit"; + expect(getChainConfig().env).toBe("summit"); + expect(getTokenSymbol()).toBe("SUM"); + expect(getNetworkLabel()).toBe("summit"); + }); +}); + /** First (primary) wss endpoint declared for a chain on an env, or undefined. */ function upstreamEndpoint(chainId: string, envId: string): string | undefined { const wss = doc.chains.find((c) => c.id === chainId)?.endpoints?.[envId]?.wss; diff --git a/src/config.ts b/src/config.ts index 195c5830..7605a244 100644 --- a/src/config.ts +++ b/src/config.ts @@ -168,7 +168,22 @@ export const CONFIGS: Partial> = { // Other envs are not wired yet — getChainConfig() throws below. }; -export function getChainConfig(env: Env = DEFAULT_ENV): ChainConfig { +/** + * Active env resolved at runtime from the `PLAYGROUND_ENV` environment variable + * (validated against `CONFIGS`), falling back to the build-time `DEFAULT_ENV`. + * + * This is the runtime counterpart to the build-time `ACTIVE_TESTNET_ENV` switch: + * because the direct-chain layer (`getConnection`, pairing, registry, drip) and + * the display helpers below all default to this, setting `PLAYGROUND_ENV` points + * a single build's own reads at a different wired network — no rebuild, no flip. + * Unset / unknown value ⇒ `DEFAULT_ENV`, so existing callers are unaffected. + */ +export function getActiveEnv(): Env { + const fromEnv = process.env.PLAYGROUND_ENV?.trim() as Env | undefined; + return fromEnv && CONFIGS[fromEnv] ? fromEnv : DEFAULT_ENV; +} + +export function getChainConfig(env: Env = getActiveEnv()): ChainConfig { const cfg = CONFIGS[env]; if (!cfg) { throw new Error( @@ -208,7 +223,7 @@ export function resolveLegacyEnv(input: string): Env { * Human-readable network label for the Header bread-crumb. Lower-cased to * match the existing visual style ("paseo", "polkadot"). */ -export function getNetworkLabel(env: Env = DEFAULT_ENV): string { +export function getNetworkLabel(env: Env = getActiveEnv()): string { switch (env) { case "paseo-next-v2": return "paseo next v2"; @@ -230,9 +245,9 @@ export function getNetworkLabel(env: Env = DEFAULT_ENV): string { /** * Native token symbol for the given env (defaults to the active env). Display * only — drives balance/drip labels via `formatPas`. Flipping - * `ACTIVE_TESTNET_ENV` (e.g. to `"summit"`) re-labels everything from here. + * `ACTIVE_TESTNET_ENV` (or setting `PLAYGROUND_ENV`) re-labels everything here. */ -export function getTokenSymbol(env: Env = DEFAULT_ENV): string { +export function getTokenSymbol(env: Env = getActiveEnv()): string { return getChainConfig(env).tokenSymbol; } @@ -241,7 +256,7 @@ export function getTokenSymbol(env: Env = DEFAULT_ENV): string { * Display only — used by `playground status` to read the product account's PGAS * balance. See `ChainConfig.pgasAssetId`. */ -export function getPgasAssetId(env: Env = DEFAULT_ENV): number { +export function getPgasAssetId(env: Env = getActiveEnv()): number { return getChainConfig(env).pgasAssetId; }