Skip to content
Open
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
13 changes: 13 additions & 0 deletions .changeset/playground-env-runtime-override.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
49 changes: 47 additions & 2 deletions src/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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;
Expand Down
25 changes: 20 additions & 5 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,22 @@ export const CONFIGS: Partial<Record<Env, ChainConfig>> = {
// 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(
Expand Down Expand Up @@ -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";
Expand All @@ -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;
}

Expand All @@ -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;
}

Expand Down
Loading