diff --git a/.changeset/fix-init-identity-lines.md b/.changeset/fix-init-identity-lines.md new file mode 100644 index 0000000..74c4ddc --- /dev/null +++ b/.changeset/fix-init-identity-lines.md @@ -0,0 +1,5 @@ +--- +"playground-cli": patch +--- + +Fix `dot init` identity block: print the full product-account SS58 and 0x-prefixed H160 instead of truncated `5DHk4g...CzE1 (0x8849...29dc)`, and fix the username lookup so it actually queries `Resources.Consumers` correctly. The previous code routed the SS58 through `AccountId().dec(...)` (which is meant for `0x`-hex input, not SS58) and silently corrupted the storage key, so every lookup surfaced as `(lookup failed)`. Now the SS58 is passed straight to `getValues`, matching the polkadot-desktop / dotli / triangle-js-sdks pattern. diff --git a/CLAUDE.md b/CLAUDE.md index 17458f1..d33aa11 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,10 +1,10 @@ # CLAUDE.md -Refer to the **Contributing** and **Architecture Highlights** sections of [README.md](./README.md) for development workflows, release process, and repo conventions. +Refer to the **Contributing** and **Architecture Highlights** sections of [README.md](./README.md) for development workflows, the release process, and repo conventions. ## Verification before committing -Before claiming a task complete, opening a PR, or merging, run these three checks. The first two are enforced by CI; the third catches regressions: +Before claiming a task complete, opening a PR, or merging, run all three. The first two are enforced by CI; the third catches regressions: ```bash pnpm format:check @@ -12,185 +12,173 @@ pnpm lint:license pnpm test ``` -`pnpm build` is the canonical type signal — there is no separate `tsc` step. If `lint:license` flags a file you authored, run `./scripts/check-license-headers.sh --fix` to prepend the standard Parity Apache-2.0 header. Every tracked `.ts` / `.tsx` / `.rs` file must carry both the `SPDX-License-Identifier: Apache-2.0` line and the `Copyright (C) Parity Technologies (UK) Ltd.` line — a bare SPDX line alone is rejected, and the `License Headers` workflow fails closed on missing headers. The check script handles shebanged scripts (`#!/usr/bin/env node|bun`) by keeping the shebang on line 1 and placing the header below it. +`pnpm build` is the canonical type signal — there is no separate `tsc` step. If `lint:license` flags a file you authored, run `./scripts/check-license-headers.sh --fix` to prepend the standard Parity Apache-2.0 header (`SPDX-License-Identifier: Apache-2.0` + `Copyright (C) Parity Technologies (UK) Ltd.`, both lines required). The check script keeps shebangs on line 1 and places the header below them. ## Non-obvious invariants -These are things that aren't self-evident from reading the code and have bitten us before: - -- **Direct imports come from `@parity/product-sdk-*`, not `@polkadot-apps/*`.** The CLI runtime is on product-sdk packages (`-address`, `-bulletin`, `-chain-client`, `-contracts`, `-descriptors`, `-keys`, `-storage`, `-terminal`, `-tx`, `-utils`). `@polkadot-apps/*` is fully gone from the lockfile (`grep '@polkadot-apps/' pnpm-lock.yaml` returns 0 hits) since `@dotdm/contracts` shipped its own product-sdk migration. Don't reintroduce direct `@polkadot-apps/*` imports — there's a `grep -rnE "['\"]@polkadot-apps/" src/ e2e/ scripts/ tools/` guard in CI's `Format` job that fails the build. Product-sdk packages use caret ranges (`^0.x.y`) so minor/patch upstream releases land on a fresh `pnpm install`; the lockfile pins the actually-resolved version. The SDK is pre-1.0 and `^` on a 0.x range only widens patches (`^0.2.0` → `>=0.2.0 <0.3.0`), so a true breaking change still requires an explicit bump in `package.json`. -- **`@dotdm/contracts` is on a caret range** (`^2.0.3`). The 2.0 line ships with `resolveTargetRegistryAddress` + re-exports `REGISTRY_ADDRESS` from `@dotdm/utils`, which `src/config.ts::CDM_REGISTRY_ADDRESS` consumes. Earlier dev-tag pins (e.g. `1.1.1-dev.…`) predated `@dotdm/contracts`'s own product-sdk migration; the legacy `1.1.1` stable still depends on `@polkadot-apps/*` + PAPI 1.x and must NOT be downgraded to. Patch bumps within 2.x are safe. -- **`@novasamatech/*` packages are forced to `0.7.9-4` via `pnpm.overrides`.** They're transitive (pulled in by `@parity/product-sdk-terminal`'s `^0.7.7` ranges) and pnpm doesn't bump transitives across patches automatically. The override aligns the whole tree on the latest published Novasama line so we get the most recent host-papp + statement-store fixes (including RFC-0010 `requestResourceAllocation` on `UserSession`). Drop the override once product-sdk-terminal bumps its caret to `^0.7.9-4` natively, or once we move our pins forward. -- **`@polkadot-api/json-rpc-provider: ^0.2.0` override is still load-bearing.** Removing it splits the lockfile across three versions of `json-rpc-provider`: `0.0.1` (optional), `0.0.4`, and `0.2.0` — different transitive consumers in the PAPI 2.x ecosystem ask for different versions. Forcing everyone onto `0.2.0` keeps a single version in the tree, which avoids subtle wire-shape divergence and reduces bundle/process memory. Originally added to consolidate `@polkadot-apps/*` consumers (now gone), but the consolidation still benefits the PAPI 2.x consumers. Drop only when every transitive consumer uplifts to a unified range. -- **Only `@parity/dotns-cli` still declares `polkadot-api@1.x`**, and that's a bundled Bun-built CLI binary (`dist/cli.js`) — its `polkadot-api@1.x` is sealed inside the bundle, never resolved from `node_modules` at runtime. We're effectively a PAPI 2.x-only process. -- **The mobile app wraps `signRaw` data with ``** (anti-phishing envelope, see `polkadot-app-android-v2/.../MessageSigningContext.kt::generalUntrustedMessage` — still load-bearing on Android nightly/v1198). On paseo-next-v2 this no longer matters for tx signing: `@parity/product-sdk-terminal@0.2.1`'s `createSessionSignerForAccount` switched from the PJS bridge to a PAPI-native signer that routes tx payloads through `session.signRaw({ data: { tag: "Payload", value: hex(toSign) } })` — opaque bytes, no `` envelope — so every signed extension declared by the chain (including paseo-next-v2's `AsPgas`) survives end-to-end. Don't reach for `signRaw` to sign extrinsic payloads from anywhere outside the signer; raw-message signing keeps the `Bytes` tag so the anti-phishing wrap stays in place for arbitrary user data. The pre-0.2.1 PJS path failed on v2 with `PJS does not support this signed-extension: AsPgas`; don't downgrade past `^0.2.1`. -- **`getSessionSigner()` returns an adapter that keeps the Node event loop alive**. Every caller must invoke the returned `destroy()` when done. If you add a new top-level command that signs on behalf of the user, wire up the cleanup or the process will hang after the work is done. -- **`dot init --yes` auto-runs at the end of `install.sh`**. The `--yes` flag skips the interactive QR-scan so non-interactive installers (CI, Docker, curl-pipe) don't block. It installs prerequisites and prints "setup complete", then `install.sh` prints a hint to run `dot init` for the full mobile login. If the dep-setup step fails, the exit code is surfaced so CI runs don't silently pass. -- **All chain URLs / contract addresses live in `src/config.ts`**. Never inline a websocket URL or an `0x…` address anywhere else — when mainnet launches we'll be flipping one switch, not grepping the tree. -- **Env model mirrors bulletin-deploy's environment ids**: `preview | paseo-next | paseo-review | paseo-next-v2 | polkadot | kusama`. `ACTIVE_TESTNET_ENV = "paseo-next-v2"` is the only env fully wired today; the rest throw "not supported" from `getChainConfig()`. The deploy command's `--env` flag accepts both the new ids AND legacy `testnet|mainnet` aliases (mapped via `resolveLegacyEnv` — `testnet` → `paseo-next-v2`, `mainnet` → `polkadot`). When adding a second env, populate the entry in `CONFIGS` (`src/config.ts`) and verify the descriptor for that chain exists in `@parity/product-sdk-descriptors` — the `paseo-asset-hub`/`paseo-bulletin`/`paseo-individuality` exports we use today are generated against paseo-next-v2 endpoints + matching genesis hashes (despite the unversioned name). -- **Allowance grant markers live at `~/.polkadot/allowances.json`** (`src/utils/allowances/marker.ts`), mode 0600, sibling to `accounts.json`. RFC-0010 has no on-chain query for allowance status, so we persist `{ env: { ss58Address: { resourceTag: { grantedAt, source } } } }` after a successful host grant. Slot-account private keys for Bulletin / Statement Store live separately in `~/.polkadot/allowance-keys.json` (`src/utils/allowances/slotKeys.ts`), also mode 0600. A marker alone is not enough to skip `dot init` for slot resources — callers must confirm the matching key exists too. Markers and keys are isolated per env so switching networks doesn't accidentally short-circuit re-grants. Keep `source: "host"` as the only value emitted from production code — the type allows `"alice"` for backfill / legacy tooling, but the v2 init flow has no Alice fallback. -- **`requestResourceAllocation` lives in a CLI-local shim** (`src/utils/allowances/host.ts`). `@parity/product-sdk-terminal@0.2.1` does NOT yet re-export the RFC-0010 host call at the package root, but the underlying `UserSession` (from `@novasamatech/host-papp`) exposes `session.requestResourceAllocation()`. We call it directly via the raw session retained on `SessionHandle.userSession`. `@parity/product-sdk-host`'s `requestResourceAllocation` is the in-container variant (uses browser globals like `window.__HOST_WEBVIEW_MARK__`) and won't work from the CLI — don't swap to it. Replace the shim with the SDK call once product-sdk-terminal surfaces it externally. -- **Deploy delegates to `bulletin-deploy` for everything storage-related** (chunking, retries, pool accounts, nonce fallback, DAG-PB, DotNS commit-reveal). We intentionally do NOT reimplement any of that here. The one thing we own is `registry.publish()` — because the contract records `env::caller()` as app owner and that needs to be the user, not a shared dev key. See `src/utils/deploy/playground.ts`. -- **Do NOT call `bulletin-deploy.deploy()` just to store a metadata JSON.** `deploy()` unconditionally runs a DotNS `register()` + `setContenthash()` for whatever name you hand it — and for `domainName: null` it invents a `test-domain-` label and registers THAT. That second DotNS pass reverts cryptically (`Contract execution would revert: 0x…`). For plain storage of the playground metadata we submit `TransactionStorage.store` directly via PAPI (`bulletinApi.tx.TransactionStorage.store({ data })`) using `calculateCid` from `@parity/product-sdk-bulletin` for the content-addressing. The metadata `store` is signed with the product-scoped RFC-0010 Bulletin allowance account cached in `allowance-keys.json`, not the product account signer and not Alice. Asset Hub `registry.publish` is still signed with the user's product account so the registry owner is correct. No DotNS side-trip. See `src/utils/deploy/playground.ts::publishToPlayground`. -- **Build a dedicated Bulletin client with `heartbeatTimeout: 300_000` for the metadata upload.** The shared client from `getConnection()` uses `@parity/product-sdk-chain-client`, which calls `getWsProvider(rpcs)` with no options → polkadot-api's 40 s default heartbeat. A single `TransactionStorage.store` round-trip can exceed 40 s and the socket tears down as `WS halt (3)`. `bulletin-deploy` sidesteps this with its own 300 s heartbeat; we mirror that with a one-off client in `src/utils/deploy/playground.ts` that we destroy immediately after the upload. -- **`dot deploy` does NOT pass `jsMerkle: true` to `bulletin-deploy` right now.** bulletin-deploy's pure-JS merkleizer produces CARs that only contain raw leaves — the DAG-PB directory/file blocks are silently dropped by `blockstore-core/memory`'s `getAll()` under `rawLeaves: true` + `wrapWithDirectory: true`. Proof: a real deployed CAR we fetched from `paseo-ipfs.polkadot.io` contained 157 raw blocks and zero DAG-PB, with the declared root absent → polkadot-desktop parses zero files → sites show `{"message":"404: Not found"}`. Until the upstream merkleizer is fixed we rely on the Kubo binary path (the default), which is reliable. `dot init` installs `ipfs`, so this Just Works for anyone who ran setup. **Trade-off**: this temporarily breaks the RevX WebContainer story for the main storage upload — flip `jsMerkle: true` back once bulletin-deploy fixes `merkleizeJS` to collect all blocks, not just leaves. +These aren't self-evident from reading the code and have bitten us before. Treat each one as a load-bearing gotcha — don't undo without checking the failure mode it prevents. + +### Dependency pins / lockfile + +- **Import from `@parity/product-sdk-*`, never `@polkadot-apps/*`.** The CLI runtime is fully on product-sdk. `@polkadot-apps/*` is gone from the lockfile and CI's `Format` job runs `grep -rnE "['\"]@polkadot-apps/" src/ e2e/ scripts/ tools/` as a guard. Product-sdk uses caret ranges (`^0.x.y`); on a 0.x line `^` only widens patches, so a true breaking change still needs an explicit `package.json` bump. +- **`@dotdm/contracts` is on `^2.0.3`.** The 2.0 line ships `resolveTargetRegistryAddress` and re-exports `REGISTRY_ADDRESS` from `@dotdm/utils`, both used by `src/config.ts::CDM_REGISTRY_ADDRESS`. The legacy `1.1.1` stable still depends on `@polkadot-apps/*` + PAPI 1.x — do NOT downgrade. +- **`@novasamatech/*` packages are forced to `0.7.9-4` via `pnpm.overrides`.** They're transitive (via `@parity/product-sdk-terminal`'s `^0.7.7` ranges) and pnpm won't bump transitives across patches. The override aligns the tree on the latest published Novasama line including RFC-0010 `requestResourceAllocation`. Drop the override once product-sdk-terminal bumps its caret natively. +- **`@polkadot-api/json-rpc-provider: ^0.2.0` override is load-bearing.** Removing it splits the lockfile across three versions of `json-rpc-provider` (`0.0.1`/`0.0.4`/`0.2.0`) — different PAPI 2.x transitive consumers ask for different versions. Forcing everyone onto `0.2.0` avoids subtle wire-shape divergence and reduces bundle/process memory. +- **`@parity/dotns-cli@0.6.1` ships a broken publish manifest** declaring `"@polkadot-api/descriptors": "file:.papi/descriptors"` — a workspace path missing from the tarball. pnpm refuses; we redirect that sub-dep to `stubs/papi-descriptors-stub/` (an empty `{}` export). dotns-cli's `dist/cli.js` is a fully-bundled Bun build, so the stub is functionally correct. Remove the override + stub when `@parity/dotns-cli` republishes a clean manifest. +- **`bulletin-deploy` is pinned to an explicit version (`0.7.24`), not `latest`.** A previous `latest` (0.6.8) had a WebSocket-heartbeat bug that tore chunk uploads down mid-flight. The pin avoids ever silently sliding onto a broken `latest`. When bumping, read release notes for changes to `deploy()`, DotNS methods, or the `DeployOptions` we use (`jsMerkle`, `signer`, `signerAddress`, `mnemonic`, `rpc`, `attributes`). Newer releases now also export environment helpers (`loadEnvironments`, `resolveEndpoints`, etc.); we don't consume them — our env table lives in `src/config.ts::CONFIGS`. +- **`polkadot-api` is `^2.1.3`** and effectively the only PAPI version in the runtime: the lockfile contains `polkadot-api@1.x` only because `@parity/dotns-cli` declares it, and dotns-cli ships as a single fully-bundled `dist/cli.js` with all deps inlined — never resolved at runtime. + +### Network / env + +- **`ACTIVE_TESTNET_ENV = "paseo-next-v2"`** (`src/config.ts`). It's the only env wired up; the others throw "not supported" from `getChainConfig()`. The deploy `--env` flag accepts both the new ids and the legacy `testnet|mainnet` aliases (mapped via `resolveLegacyEnv`). When adding an env, populate `CONFIGS` 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. +- **All chain URLs / contract addresses live in `src/config.ts`.** Never inline a websocket URL or `0x…` address anywhere else — at mainnet launch we'll flip one switch, not grep the tree. +- **Username lookup hits `Resources.Consumers` on the People parachain** (`src/utils/username.ts`). Mirrors `@novasamatech/host-papp`'s `createIdentityRpcAdapter`. Pass the SS58 string directly to `getValues([[ss58]])` — do NOT round-trip through `AccountId().dec(ss58)`. The upstream code only does that because its callers pass `0x…` hex; on an SS58 string `Bytes(32).dec` silently corrupts it into a different 32-byte sequence and the lookup fails opaquely as `(lookup failed)`. + +### Deploy / Bulletin + +- **Deploy delegates to `bulletin-deploy` for everything storage-related** — chunking, retries, pool accounts, nonce fallback, DAG-PB, DotNS commit-reveal. Don't reimplement. The one thing we own is `registry.publish()`: the contract records `env::caller()` as app owner and that must be the user, not a shared dev key. See `src/utils/deploy/playground.ts`. +- **Do NOT call `bulletin-deploy.deploy()` just to store a metadata JSON.** `deploy()` unconditionally runs a DotNS `register()` + `setContenthash()`, and for `domainName: null` invents a `test-domain-` label and registers THAT — the side-trip reverts cryptically. For metadata storage we submit `TransactionStorage.store` directly via PAPI using `calculateCid` from `@parity/product-sdk-bulletin`. The metadata `store` is signed with the product-scoped RFC-0010 Bulletin allowance account cached in `allowance-keys.json` (not Alice, not the product account). Asset Hub `registry.publish` is still signed with the user's product account so the registry owner is correct. See `src/utils/deploy/playground.ts::publishToPlayground`. +- **Build a dedicated Bulletin client with `heartbeatTimeout: 300_000` for the metadata upload.** The shared client from `getConnection()` uses `@parity/product-sdk-chain-client`'s default 40 s heartbeat; a single `TransactionStorage.store` round-trip can exceed that and the socket tears down as `WS halt (3)`. We mirror bulletin-deploy's 300 s heartbeat with a one-off client that gets destroyed immediately after the upload. +- **`dot deploy` does NOT pass `jsMerkle: true` today.** bulletin-deploy's pure-JS merkleizer produces CARs containing only raw leaves (DAG-PB blocks are silently dropped by `blockstore-core/memory`'s `getAll()` under `rawLeaves: true` + `wrapWithDirectory: true`) → polkadot-desktop parses zero files → sites return 404. We rely on the Kubo binary path until the upstream merkleizer collects all blocks, not just leaves. `dot init` installs `ipfs`. Trade-off: this temporarily breaks the RevX WebContainer story for the main storage upload — flip `jsMerkle: true` back once `merkleizeJS` is fixed. +- **The mobile app wraps `signRaw` data with ``** (anti-phishing envelope). On paseo-next-v2 this doesn't matter for tx signing: `@parity/product-sdk-terminal@0.2.1+`'s `createSessionSignerForAccount` routes tx payloads through `session.signRaw({ data: { tag: "Payload", value: hex(toSign) } })` — opaque bytes, no `` envelope — so every signed extension declared by the chain (including paseo-next-v2's `AsPgas`) survives end-to-end. Don't reach for `signRaw` to sign extrinsic payloads from anywhere outside the signer; raw-message signing keeps the `Bytes` tag for arbitrary user data. The pre-0.2.1 PJS path failed on v2 with `PJS does not support this signed-extension: AsPgas`. - **Signer mode selection lives in one file** (`src/utils/deploy/signerMode.ts`). The mainnet rewrite is a single-file swap; keep that boundary clean. -- **`src/utils/deploy/*` and `src/utils/build/*` must not import React or Ink.** They form the SDK surface that RevX consumes from a WebContainer. TUI code lives in `src/commands/*/`. -- **Bun compiled-binary stdin quirk** — Ink's `useInput` silently drops every keystroke (arrows, Enter, Ctrl+C) in `bun build --compile` binaries unless `process.stdin.on('readable', …)` is touched before Ink's `render()`. We install a no-op `readable` listener at the top of `src/index.ts` as a warm-up. Do NOT remove it until Bun's compiled-binary TTY stdin behaves like Node's. Symptom if this breaks: TUI renders but nothing responds, including Ctrl+C. -- **`bulletin-deploy` 0.7.4+ pulls in a transitive dep with a broken publish manifest** that pnpm refuses to install. `@parity/dotns-cli` (0.6.0 and 0.6.1 both) publishes a `package.json` declaring `"@polkadot-api/descriptors": "file:.papi/descriptors"` — a workspace-only path that doesn't exist in the published tarball. npm tolerates the dangling `file:` reference (creates a broken symlink and continues); pnpm's strict resolver fails with `ERR_PNPM_LINKED_PKG_DIR_NOT_FOUND`. We work around it with a `pnpm.overrides` entry in `package.json` pointing the offending sub-dep at a tiny stub package (`stubs/papi-descriptors-stub/`) so resolution succeeds. The dep is functionally vestigial — dotns-cli's `dist/cli.js` is fully-bundled (Bun build, no externals) and never imports `@polkadot-api/descriptors` at runtime, so the stub exporting `{}` is correct. **Remove the override + stub once `@parity/dotns-cli` republishes a clean manifest.** Tracked upstream against `paritytech/dotns-sdk`. Our direct pin is at the same exact version `bulletin-deploy@0.7.13` declares (`^0.6.1` → `0.6.1`) so both top-level resolution (used by Bun's file-import bundling in `src/dotns-cli-dispatch.ts`) and bulletin-deploy's runtime `_require.resolve("@parity/dotns-cli")` land on the same tarball. -- **`bulletin-deploy` is pinned to an explicit version, not `latest`.** We're on `0.7.20` stable today. The `latest` npm dist-tag is a moving target and previously pointed at 0.6.8, which has a WebSocket heartbeat bug (default 40s < chunk timeout 60s) that tears down uploads mid-flight as `WS halt (3)`. Keep the pin explicit so we never silently slide onto a broken `latest`. When upgrading, read the release notes for any public-API changes to `deploy()`, `DotNS` methods, or the `DeployOptions` we rely on (`jsMerkle`, `signer`, `signerAddress`, `mnemonic`, `rpc`, `attributes`). Note: 0.7.0 removed the `playground?: boolean` `DeployOption` (registry publishing now lives here in `src/utils/deploy/playground.ts`), which is a no-op for us since we never passed that flag. 0.7.1 made the memory-report teardown Bun-safe upstream. 0.7.2 bumped the default `CHUNK_TIMEOUT_MS` 60s → 180s to match Bulletin's new 24s Aura slot duration; `BULLETIN_CHUNK_TIMEOUT_MS` override still works. 0.7.4 extracted the dotns logic into a separate `@parity/dotns-cli` subprocess (forked via `_require.resolve("@parity/dotns-cli")`); see the publish-bug workaround note above. 0.7.4 also moved label classification off the `DotNS` instance — the previously-instance method `dotns.classifyName(label)` is now the top-level pure function `classifyDotnsLabel(label)`, and the result field renamed `requiredStatus` → `status`. The function isn't re-exported from the package root, so `src/utils/deploy/availability.ts` mirrors the (small, stable) logic locally as `classifyLabel` — same precedent as `simulateUserStatus`. 0.7.6 added ambient Sentry mode for host apps; keep the CLI-owned privacy gate in `src/bootstrap.ts`. 0.7.9 includes the DotNS/deploy fixes needed by the CDM E2E path. 0.7.13 added a `--env ` selector to the `bulletin-deploy` CLI binary (paseo-next default; preview, paseo-review, polkadot, kusama) plus three additive deploy span attributes (`deploy.env`, `deploy.network`, `deploy.environments_source`); zero behaviour change for library consumers like us — we keep using `--rpc` / `BULLETIN_RPC` precedence and the default endpoint resolves to the same paseo-next WSS as before. 0.7.14 hardens the chunked-storage path against WS-halt allocation storms (issues #142/#216/#287): a per-deploy retry-budget circuit breaker (defaults: 5 events / 30s, tunable via `BULLETIN_RETRY_BUDGET_MAX` and `BULLETIN_RETRY_BUDGET_WINDOW_MS`) bails with a clear `Retry budget exhausted: …` error rather than letting GC fall behind; recovery batch size drops from 2-in-flight to 1-in-flight after the first reconnect; and a synchronous `onStatusChanged(CLOSE|ERROR)` hook destroys the PAPI client before its `activeBroadcasts.forEach` loop can mutate-while-iterating into OOM. Public surface (`deploy`, `DeployContent`, `DeployOptions`, `DeployResult`) is unchanged; the new exports `setWsHaltCallback` / `retryBudgetExhausted` / `isConnectionError` are internal utilities we don't import. Telemetry now sets `deploy.status="ok"` on the success path (we already get `error`/`killed` from #289). The previously-suspect `new Uint8Array(fs.readFileSync(...))` double-wrap is gone (perf-only). Our metadata-upload bypass via PAPI's `TransactionStorage.store` is unaffected — these changes only live inside `storeChunkedContent` / `deploy()`. 0.7.15–0.7.18 are internal hardening / Sentry instrumentation; no public API changes. 0.7.19 added the `paseo-next-v2` environment to `assets/environments.json` (Asset Hub Next paraId 1500, Bulletin Next 1501, People Next System 1502, identity backend at `identity-backend-next.parity-testnet.parity.io`) with `autoAccountMapping: true`, `bulletinAuthorizeV2: true`, `skipDotnsCli: true` (so `deploy()` runs DotNS register/setContenthash directly via on-chain contracts at the v2 addresses rather than spawning the `dotns-cli` subprocess — `dotns-cli@0.6.1` has stable-Paseo addresses hardcoded). The `paseo-next-v2` env flows through `deploy({ env: "paseo-next-v2" })` from `src/utils/deploy/run.ts`; bulletin-deploy reads the env entry to pick endpoints, contract addresses, and feature flags. 0.7.20 (PRs #357 + #369) hardens the v2 path in two ways relevant to us. First, the environment catalog is now embedded into the built JS bundle as a fallback, so `loadEnvironments()` still resolves full v2 metadata (DotNS contract addresses, `autoAccountMapping`, `bulletinAuthorizeV2`, `skipDotnsCli`) when the package's `assets/environments.json` can't be read from disk (Bun SEA contexts, sandboxed deploys, etc.). Second, `DotNS.connect({ autoAccountMapping: true })` now extracts the trigger logic into `ensureAutoMappedAccountReady()`, which on testnets first reads the signer's free balance and, if it's below `FEE_FLOOR_REGISTER` (0.1 PAS), calls `attemptTestnetTopUp(addr, TOP_UP_TARGET=0.5 PAS)` — iterating bare-master ("Alice") then `//Bob` from the standard dev mnemonic with a 1 PAS source-buffer floor — BEFORE submitting the Revive.call auto-map trigger. If mapping still doesn't take, the error message points at `https://faucet.polkadot.io`. The bare-master source is the SAME account our `src/utils/account/bulletinTopUp.ts` uses in `dot init`, so the two flows are aligned: our `dot init` front-loads the top-up at setup time, and bulletin-deploy now does it just-in-time during `dot deploy` for users who skip `dot init`. Both no-op when the recipient is already ≥ 0.1 PAS, so running both back-to-back doesn't double-transfer. Replace `src/utils/account/bulletinTopUp.ts` with the in-SDK call only if bulletin-deploy ever surfaces `attemptTestnetTopUp` at the package root — it's currently an internal method on `DotNS`. 0.7.20 also exports environment helpers from the package root for the first time: `loadEnvironments`, `resolveEndpoints`, `listEnvironments`, `formatEnvironmentTable`, `defaultBundledPath`, `DEFAULT_ENV_ID` (plus matching types `Chain`, `ChainEndpoint`, `Environment`, `EnvironmentListing`, `EnvironmentsDoc`, `EnvironmentsSource`, `LoadOptions`, `LoadResult`, `ResolvedEndpoints`). We don't consume these today (our env table lives in `src/config.ts::CONFIGS`), but they're available if we ever want to drop the parallel config and read v2 endpoints / contract addresses directly from bulletin-deploy's catalog. - -### product-sdk 0.5.0 (2026-05-13 workspace release) - -- **`@parity/product-sdk-terminal@0.2.1`** ships the PAPI-native signer (#81), unblocking paseo-next-v2's `AsPgas` signed extension. Call sites are unchanged. -- **`@parity/product-sdk-contracts@0.5.0`** makes `QueryResult` a **discriminated union**: callers MUST narrow on `.success` before reading `.value`. On the failure branch `.value` is the runtime's dispatch-error payload (`unknown`). On the success branch `gasRequired` is now non-optional. We apply this in `src/utils/contractManifest.ts::resolveLiveContractAddresses`, `src/commands/mod/AppBrowser.tsx` (failed `getApps.query` → empty list, stop paginating), and `src/commands/mod/SetupScreen.tsx` (failed `getMetadataUri.query` → clear error). -- **`@parity/product-sdk-host@0.3.0`** is a new package exposing `requestResourceAllocation` for in-container apps (browser globals required). We list it as a dep for type reference but **do not import** it from runtime code; the CLI is external to the host container and goes through the `UserSession`-based shim in `src/utils/allowances/host.ts` instead. -- **Throttle TUI info updates** — bulletin-deploy logs per-chunk and builds (vite/next) stream thousands of lines/sec. Calling `setState` on every log event floods React's reconciler with so much backpressure the process can balloon past 20 GB and freeze the OS. `RunningStage` coalesces "latest info" updates to ≤10/sec via a ref + timer and caps line length at 160 chars. Any new hot-path event sink should do the same; don't hook raw per-line streams directly into Ink state. -- **Process-guard safety net** (`src/utils/process-guard.ts`) — deploy pipelines open several long-lived WebSockets + child processes and any one of them can keep the event loop alive after the TUI visibly finishes, turning `dot` into a zombie that accumulates retry buffers indefinitely (seen climbing past 25 GB). We defend in depth: (1) `installSignalHandlers()` catches SIGINT/TERM/HUP + `unhandledRejection` and forces cleanup + exit within 3 s. The `unhandledRejection` handler runs each rejection through `isBenignUnsubscriptionError`, which suppresses three known post-destroy artifacts: rxjs `UnsubscriptionError` wrapping `Not connected`, polkadot-api `DisjointError` from a chainHead unfollow race, and PAPI's `DestroyedError("Client destroyed")` raised when a stray subscription tries to send a final RPC after the lazy client has already been torn down. The 0.2.0 `@parity/product-sdk-terminal::destroy()` fix drains pending unsubscribes — but ONLY when the caller awaits. Our `SessionHandle.destroy()` returns void (so React `useEffect` cleanup can call it), and inside it we do `adapter.destroy().catch(() => {})` — fire-and-forget with the rejection silenced at the source. The source-side `.catch()` is load-bearing: Bun's compiled SEA binary prints `unhandledRejection` events REGARDLESS of any `process.on('unhandledRejection')` listener, so the `isBenignUnsubscriptionError` filter only stops our own additional stderr write — it does NOT stop Bun from printing the rejection itself. The duplicated suppression in `isBenignUnsubscriptionError` covers Node-runtime tests; the `.catch(() => {})` at each `adapter.destroy()` callsite (`src/utils/auth.ts:146,252,268` style) covers Bun runtime. Drop the source-side `.catch()` only once `SessionHandle.destroy()` is fully awaitable end-to-end, or once Bun respects `process.on('unhandledRejection')` in SEA binaries; (2) `scheduleHardExit()` installs an `unref`'d timer that kills the process if the event loop doesn't drain within a grace period; (3) `startMemoryWatchdog()` aborts if RSS exceeds 4 GB — a generous cap because legit deploys on Bun SEA binaries routinely touch 1–1.5 GB from runtime-metadata decoding + Bun's JSC heap + Ink yoga. Do NOT re-add a per-window growth detector: we tried 300 MB / 3 s and it false-positived on the single-burst metadata-loading spike, aborting deploys that would have succeeded. Set `DOT_MEMORY_TRACE=1` to stream per-sample RSS/heap/external stats — useful when diagnosing a real leak report. **Telemetry bootstrap** (`src/bootstrap.ts`) is the FIRST import in `src/index.ts`. It sets `BULLETIN_DEPLOY_USE_AMBIENT_SENTRY=1` and `BULLETIN_DEPLOY_HOST_APP=playground-cli` before `bulletin-deploy` can evaluate, then maps `DOT_TELEMETRY`/internal-context detection to `BULLETIN_DEPLOY_TELEMETRY`. Do not leave `BULLETIN_DEPLOY_TELEMETRY` unset while setting the host app: `bulletin-deploy` treats `playground-cli` as an internal host, which would enable deploy telemetry for external users. `BULLETIN_DEPLOY_MEM_REPORT` is not forced off by default anymore because upstream guards the Bun-incompatible memory-report path. Any new long-running command should register a cleanup hook via `onProcessShutdown()`. -- **Parser MUST NOT emit an event per log line.** `DeployLogParser.feed()` is called for every console line bulletin-deploy prints — hundreds per deploy on the happy path, thousands if retries fire. We intentionally emit events ONLY for phase-banner matches and `[N/M]` chunk progress. Everything else returns `null`. Adding a catch-all `info` emit turns the parser into a firehose that allocates ~200 bytes × thousands of lines, and was a measurable contributor to chunk-upload memory pressure. -- **`dot mod` runs signer-less.** `runModCommand` does not call `resolveSigner` — it gets a read-only handle via `getReadOnlyRegistryContract(rawClient)` (origin = Alice's SS58 derived from `getDevPublicKey("Alice")`) and uses it for both browse + metadata-uri lookup. The `--suri` flag is retained as a deprecated no-op for back-compat. Users can browse and clone moddable apps without first running `dot init` / mapping their account. The signed `getRegistryContract(rawClient, signer)` is used only for `registry.publish.tx(...)` in `src/utils/deploy/playground.ts`. Don't drag a user signer back into `dot mod` — it adds a login + map-account gate before the user has even decided whether to mod the app. -- **`dot mod` is GitHub-tarball-only and must stay that way.** `src/utils/mod/source.ts` downloads from `codeload.github.com` (no auth, no `git`/`gh` required for the public-repo case) and extracts via `node:zlib` + the pure-JS `tar` package. Do NOT re-introduce `git clone` or `gh repo fork` paths — both would re-add a hard tooling requirement and the fork path was specifically removed because GitHub caps you to one fork per source-repo per account, which broke "mod the same starter twice." A non-moddable app (no `metadata.repository`) returns a hard error from `dot mod`; the interactive picker filters those out so the user never sees an unmoddable option. The picker does NOT pre-probe each app's repo visibility, because that would burn the 60 req/hr anonymous GitHub API quota on every `dot mod`. Instead, `runModCommand` lazy-probes the picked app once via `assertPublicGitHubRepo()` between picker dismount and `SetupScreen` mount; `dot deploy --moddable` already rejects private repos at deploy time, so this fires only when a publisher has flipped visibility post-publish. -- **`dot` never invokes `gh`.** `dot deploy --moddable` reads an existing `origin`, validates it's a public GitHub URL via `HEAD https://github.com/{o}/{r}`, and records it in metadata. There is no auto-create path: no `gh` install, no `gh auth status` check, no `gh repo create`. Missing `origin`, private repos, and non-GitHub URLs all hard-fail with actionable messages from `src/utils/deploy/moddable.ts::resolveRepositoryUrl()`. We deliberately do NOT add an interactive `gh auth login` handoff — Ink owns stdout + raw-mode stdin and a `stdio: "inherit"` child would race `useInput` for keystrokes. The user is expected to set up the public GitHub repo themselves before re-running. Do not re-introduce a `gh` dependency or any auto-create path: it tangles `dot` with one source-host's CLI, surprises users with public repos created on their account, and the interactive-auth handoff is a known footgun. -- **`metadata.repository` is set ONLY when `--moddable` is opted in.** Older code in `publishToPlayground` would silently probe `git remote get-url origin` and stuff whatever it found into the metadata, which surprised users who didn't realise their fork was being advertised. The contract: `runDeploy` takes an explicit `repositoryUrl: string | null`, and `publishToPlayground` writes the field iff that param is non-null. The CLI command is responsible for resolving the URL upstream via `src/utils/deploy/moddable.ts::resolveRepositoryUrl()`, which uses an existing public GitHub `origin` URL or fails — it never pushes or creates anything on behalf of the user. -- **`startMemoryWatchdog()` runs for both `dot deploy` and `dot mod`.** Mod's tarball download is a streaming pipe through `node:zlib` + `tar.extract()`, and a stuck IPFS gateway or a malformed tarball can leak buffers. Same 4 GB cap, same worker-thread sampler. Any new top-level command that does meaningful I/O should also call `startMemoryWatchdog()` and register `stopWatchdog` via `onProcessShutdown()`. +- **`src/utils/account/bulletinTopUp.ts` mirrors bulletin-deploy's internal `attemptTestnetTopUp`** so `dot init` front-loads the dev-funder top-up at setup time rather than waiting for the just-in-time call inside `deploy()`. Both flows no-op once the recipient is ≥ 0.1 PAS, so running them back-to-back doesn't double-transfer. Delete the local mirror only once bulletin-deploy surfaces `attemptTestnetTopUp` at the package root — today it's an internal `DotNS` method. + +### Allowances / session + +- **`getSessionSigner()` returns an adapter that keeps the Node event loop alive.** Every caller must invoke the returned `destroy()` when done. Forgetting it manifests as `dot ` hanging after the work visibly finishes. +- **`requestResourceAllocation` lives in a CLI-local shim** (`src/utils/allowances/host.ts`). `@parity/product-sdk-terminal@0.2.1` does NOT yet re-export the RFC-0010 host call at the package root, but the underlying `UserSession` (from `@novasamatech/host-papp`) does — we call it directly via the raw session on `SessionHandle.userSession`. `@parity/product-sdk-host`'s `requestResourceAllocation` is the in-container variant (browser globals required) and won't work from the CLI. Replace the shim when product-sdk-terminal surfaces it externally. +- **Allowance grant markers live at `~/.polkadot/allowances.json`** (`src/utils/allowances/marker.ts`), mode 0600, sibling to `accounts.json`. RFC-0010 has no on-chain query for allowance status, so we persist `{ env: { ss58Address: { resourceTag: { grantedAt, source } } } }` after a successful host grant. Slot-account private keys for Bulletin / Statement Store live separately in `~/.polkadot/allowance-keys.json` (`src/utils/allowances/slotKeys.ts`), also mode 0600. A marker alone isn't enough to skip `dot init` for slot resources — confirm the matching key exists too. Markers and keys are isolated per env. Keep `source: "host"` as the only value emitted from production code. +- **`dot init --yes` auto-runs at the end of `install.sh`** to skip the interactive QR-scan so non-interactive installers don't block. It installs prerequisites and prints "setup complete", then `install.sh` prints a hint to run `dot init` for the full mobile login. Dep-setup failures surface their exit code so CI runs don't silently pass. + +### CLI surface boundaries + +- **`src/utils/deploy/*` and `src/utils/build/*` must not import React or Ink.** They form the SDK surface RevX consumes from a WebContainer. TUI code lives in `src/commands/*/`. +- **`dot mod` runs signer-less.** `runModCommand` does not call `resolveSigner` — it uses `getReadOnlyRegistryContract(rawClient)` (origin = Alice's SS58) for browse + metadata-uri lookup. The `--suri` flag is a deprecated no-op. Users browse + clone moddable apps without `dot init` / mapping their account. The signed `getRegistryContract(rawClient, signer)` is used only for `registry.publish.tx(...)` in `src/utils/deploy/playground.ts`. Don't drag a user signer back into `dot mod`. +- **`dot mod` is GitHub-tarball-only and must stay that way.** `src/utils/mod/source.ts` downloads from `codeload.github.com` (no auth, no `git`/`gh` for public repos) and extracts via `node:zlib` + the pure-JS `tar` package. Do NOT re-introduce `git clone` or `gh repo fork` — both re-add a hard tooling dep, and the fork path was specifically removed because GitHub caps you to one fork per source-repo per account. The interactive picker filters out non-moddable apps. The picker does NOT pre-probe each app's repo visibility (would burn the 60 req/hr anonymous GitHub quota); instead `runModCommand` lazy-probes the picked app once via `assertPublicGitHubRepo()` between picker dismount and `SetupScreen` mount. +- **`dot` never invokes `gh`.** `dot deploy --moddable` reads an existing `origin`, validates it's a public GitHub URL via `HEAD https://github.com/{o}/{r}`, and records it in metadata. No auto-create path. Missing `origin`, private repos, and non-GitHub URLs all hard-fail with actionable messages from `src/utils/deploy/moddable.ts::resolveRepositoryUrl()`. We deliberately do NOT add an interactive `gh auth login` handoff — Ink owns stdout + raw-mode stdin and a `stdio: "inherit"` child would race `useInput` for keystrokes. +- **`metadata.repository` is set ONLY when `--moddable` is opted in.** `runDeploy` takes an explicit `repositoryUrl: string | null` and `publishToPlayground` writes the field iff that param is non-null. Earlier code silently probed `git remote get-url origin` and surprised users — don't reintroduce that behaviour. + +### Runtime / memory + +- **Bun compiled-binary stdin quirk** — Ink's `useInput` silently drops every keystroke in `bun build --compile` binaries unless `process.stdin.on('readable', …)` is touched before Ink's `render()`. We install a no-op `readable` listener at the top of `src/index.ts` as a warm-up. Symptom if this breaks: TUI renders but nothing responds, including Ctrl+C. +- **Process-guard safety net** (`src/utils/process-guard.ts`) — deploy pipelines open long-lived WebSockets + child processes; any one can keep the event loop alive after the TUI finishes, turning `dot` into a zombie. We defend in depth: (1) `installSignalHandlers()` catches SIGINT/TERM/HUP + `unhandledRejection` and forces cleanup + exit within 3 s. The rejection handler runs each rejection through `isBenignUnsubscriptionError`, which suppresses three known post-destroy artifacts (rxjs `UnsubscriptionError("Not connected")`, PAPI `DisjointError` from a chainHead unfollow race, PAPI's `DestroyedError("Client destroyed")`). Our `SessionHandle.destroy()` returns void (so React `useEffect` cleanups can call it) and fires `adapter.destroy().catch(() => {})` — fire-and-forget with the rejection silenced at the source. The source-side `.catch()` is load-bearing because Bun's SEA binary prints `unhandledRejection` events regardless of any process listener — the catch is the only way to suppress it. (2) `scheduleHardExit()` installs an `unref`'d timer that kills the process if the loop doesn't drain in time. (3) `startMemoryWatchdog()` aborts if RSS exceeds 4 GB. Do NOT re-add a per-window growth detector — we tried 300 MB / 3 s and it false-positived on the single-burst metadata-loading spike. Set `DOT_MEMORY_TRACE=1` to stream per-sample RSS/heap/external stats. +- **Telemetry bootstrap** (`src/bootstrap.ts`) is the FIRST import in `src/index.ts`. It sets `BULLETIN_DEPLOY_USE_AMBIENT_SENTRY=1` and `BULLETIN_DEPLOY_HOST_APP=playground-cli` before `bulletin-deploy` evaluates, then maps `DOT_TELEMETRY`/internal-context detection to `BULLETIN_DEPLOY_TELEMETRY`. Don't leave `BULLETIN_DEPLOY_TELEMETRY` unset while setting the host app: `bulletin-deploy` treats `playground-cli` as an internal host, which would enable deploy telemetry for external users. +- **Throttle TUI info updates.** bulletin-deploy logs per-chunk, builds stream thousands of lines/sec. `setState`-per-event floods React's reconciler with backpressure (can balloon past 20 GB and freeze the OS). `RunningStage` coalesces "latest info" updates to ≤10/sec via a ref + timer and caps line length at 160 chars. Don't hook raw per-line streams directly into Ink state. +- **`DeployLogParser.feed()` MUST NOT emit an event per log line.** It's called for every console line bulletin-deploy prints. We emit only for phase-banner matches and `[N/M]` chunk progress; everything else returns `null`. A catch-all `info` emit allocates ~200 bytes × thousands of lines and was a measurable contributor to chunk-upload memory pressure. +- **`startMemoryWatchdog()` runs for both `dot deploy` and `dot mod`.** Mod's tarball download is a streaming pipe through `node:zlib` + `tar.extract()`; a stuck IPFS gateway or malformed tarball can leak buffers. Any new top-level command doing meaningful I/O should also call `startMemoryWatchdog()` and register `stopWatchdog` via `onProcessShutdown()`. +- **`QueryResult` from `@parity/product-sdk-contracts@0.5+` is a discriminated union.** Narrow on `.success` before reading `.value`. On the failure branch `.value` is the runtime's dispatch-error payload (`unknown`). On the success branch `gasRequired` is non-optional. We apply this in `src/utils/contractManifest.ts::resolveLiveContractAddresses`, `src/commands/mod/AppBrowser.tsx`, and `src/commands/mod/SetupScreen.tsx`. ## Repo conventions -- **Every user-facing PR must include a changeset.** Releases are automated via `.github/workflows/release.yml`, but the workflow is a no-op unless a `.changeset/*.md` file exists on merge. Create one with `pnpm changeset` (or write `.changeset/.md` by hand — frontmatter: `"playground-cli": patch|minor|major`, body: user-visible summary). Pure refactors / test-only changes can skip it. +- **Every user-facing PR must include a changeset.** Releases are automated via `.github/workflows/release.yml`, which is a no-op unless a `.changeset/*.md` file exists on merge. Create one with `pnpm changeset` or by hand (frontmatter: `"playground-cli": patch|minor|major`, body: user-visible summary). Pure refactors / test-only changes can skip it. - Tests are `*.test.ts` next to the source. `vitest.config.ts` only picks up `.test.ts`; if you add `.tsx` tests update the config too. -- Pure logic that lives inside a `.tsx` component should be lifted into a sibling `.ts` file (see `completion.ts` next to `InitScreen.tsx`, or the `formatPas`/`formatMb` exports in `AccountSetup.tsx`). Tests can then import it without dragging React + Ink into the vitest runner. -- Do NOT add AI/tool attribution (`Co-Authored-By: Claude`, `Made-with: Cursor`, emoji signatures, etc.) to commits, PRs, or generated files. Never embed your name, identity, or tooling provenance anywhere in the repo. -- Do NOT commit design docs, brainstorming notes, or context dumps (e.g. `context.md`) to the repo. They belong in tickets or scratch files outside the tree. +- Pure logic inside a `.tsx` should be lifted into a sibling `.ts` file (`completion.ts` next to `InitScreen.tsx`; `identityLine.ts` next to `IdentityLines.tsx`; `formatPas`/`formatMb` exports in `AccountSetup.tsx`). Tests can then import it without dragging React + Ink into vitest. +- Do NOT add AI/tool attribution (`Co-Authored-By: Claude`, "Made with Cursor", emoji signatures) to commits, PRs, or generated files. Never embed your name, identity, or tooling provenance anywhere in the repo. +- Do NOT commit design docs, brainstorming notes, or context dumps (e.g. `context.md`) to the repo — tickets or scratch files outside the tree. - Don't mock primitives from `polkadot-api` (`Enum`, encoders) in tests — doing so turns intended coverage into tautology. -- Long-lived resources (`TerminalAdapter`, `PaseoClient`) have explicit `destroy()` / `destroyConnection()` — always release them, especially from React `useEffect` cleanups. The WebSocket keeps the event loop alive; forgetting a destroy manifests as `dot ` hanging after its work is visibly finished. +- Long-lived resources (`TerminalAdapter`, `PaseoClient`) have explicit `destroy()` / `destroyConnection()` — always release them, especially from React `useEffect` cleanups. The WebSocket keeps the event loop alive; forgetting a destroy manifests as `dot ` hanging after the work is visibly finished. ## Sentry telemetry -- DSN: `src/telemetry-config.ts::PLAYGROUND_SENTRY_DSN`. Region: EU (`https://de.sentry.io`). -- Org slug: `paritytech`. API token: macOS keychain service `sentry-api-token` (member of paritytech org with `org:read` + `org:write`). -- Attribute prefix: `cli.` (see `getCliRootAttributes` in `src/telemetry-config.ts`). Spec: `sentry-instrumentation-spec.md` at the repo root (untracked — keep there). -- **Helpers (don't reimplement):** `src/telemetry.ts` exports `withCommandTelemetry`, `withRootSpan`, `withSpan` (3-arg `(op, name, fn)` + 4-arg `(op, name, attributes, fn)` overloads), `captureWarning`, `captureException`, `errorMessage`, `sanitizedErrorMessage`. `src/utils/deploy/phase.ts` exports `withDeployPhase` for deploy-phase orchestration. `src/cli-runtime.ts` exports `runCliCommand` for the standard CLI scaffolding (telemetry + watchdog + hard-exit). Every command's `.action()` body should be one `runCliCommand(name, options, async () => { ... })` call — do not re-add try/finally + `scheduleHardExit` boilerplate. Today: `init` runs without `hardExit`/`watchdog`; `build`, `update`, `logout` run with `hardExit` only; `deploy` and `mod` run with both `watchdog` + `hardExit`. -- **Dashboards** live as JSON snapshots under `sentry/dashboards/.json`: - - `2143100.json` — **Playground CLI Health** (production filter `!cli.tag:e2e-*`). - - `2216067.json` — **Playground CLI Failures** (per-error-type drill-downs). - - `2216096.json` — **Playground CLI E2E Health** (inverse filter, `cli.tag:e2e-*`). -- **Workflow:** run `./sentry/backup-dashboards.sh` BEFORE any change. Use `./sentry/patch-dashboard.py ` for surgical edits (supports `replace`, `patch_query`, `set_description` ops) or full widget replacement. Use `./sentry/create-dashboard.py ` for new dashboards. Per spec §15f, do NOT include a `projects` field in POST payloads. Per spec §15g, PUT replaces the whole widget list — backup first. -- **E2E tagging:** every spawn from `e2e/cli/helpers/dot.ts` injects `DOT_TAG=e2e-local` (fallback), `DOT_TELEMETRY=1`, and `DEPLOY_TAG=e2e-cli-local` (derived from `DOT_TAG` with an `e2e-cli-` prefix). `tools/e2e-local.sh` overrides `DOT_TAG` to `e2e-local-{smoke|pr|nightly}`, which makes `DEPLOY_TAG` become `e2e-cli-local-{smoke|pr|nightly}`. CI sets `DOT_TAG=e2e-ci-{pr|nightly|dispatch}`, making `DEPLOY_TAG=e2e-cli-ci-{pr|nightly|dispatch}`. The `e2e-cli-` prefix on `DEPLOY_TAG` distinguishes playground-CLI E2E traffic from bulletin-deploy's own E2E suite (which uses bare `e2e-*` tags) in bulletin-deploy's telemetry dashboards. Production health widgets filter cleanly via `!cli.tag:e2e-*` (playground-CLI's Sentry) and `!deploy.tag:e2e-*` (bulletin-deploy's Sentry). -- **SAD% propagation** is verified by a regression test in `src/telemetry.test.ts` ("SAD% propagation through transaction envelope"). It confirms `captureWarning` flips `cli.sad="true"` on the root transaction. If that test fails, the SAD% dashboard widget on Dashboard 1 will silently degrade to a duplicate of the unexpected-failure rate. +- DSN: `src/telemetry-config.ts::PLAYGROUND_SENTRY_DSN`. Region: EU (`https://de.sentry.io`). Attribute prefix: `cli.`. Spec: `sentry-instrumentation-spec.md` at the repo root (untracked). +- Org slug: `paritytech`. API token: macOS keychain service `sentry-api-token`. +- **Helpers — don't reimplement.** `src/telemetry.ts` exports `withCommandTelemetry`, `withRootSpan`, `withSpan` (3-arg `(op, name, fn)` and 4-arg `(op, name, attrs, fn)` overloads), `captureWarning`, `captureException`, `errorMessage`, `sanitizedErrorMessage`. `src/utils/deploy/phase.ts` exports `withDeployPhase`. `src/cli-runtime.ts` exports `runCliCommand` — every command's `.action()` body should be one `runCliCommand(name, options, async () => { ... })` call. Today: `init` runs without `hardExit`/`watchdog`; `build`, `update`, `logout` run with `hardExit` only; `deploy` and `mod` run with both. +- **Dashboards** are JSON snapshots under `sentry/dashboards/.json`: `2143100` (Health, prod filter `!cli.tag:e2e-*`), `2216067` (Failures), `2216096` (E2E Health, inverse filter `cli.tag:e2e-*`). +- **Workflow:** run `./sentry/backup-dashboards.sh` BEFORE any change. Use `./sentry/patch-dashboard.py ` for surgical edits or `./sentry/create-dashboard.py ` for new dashboards. PUT replaces the whole widget list — backup first. Don't include a `projects` field in POST payloads. +- **E2E tagging:** every spawn from `e2e/cli/helpers/dot.ts` injects `DOT_TAG=e2e-local` (fallback), `DOT_TELEMETRY=1`, and `DEPLOY_TAG=e2e-cli-local` (derived from `DOT_TAG` with an `e2e-cli-` prefix). `tools/e2e-local.sh` overrides `DOT_TAG` to `e2e-local-{smoke|pr|nightly}`; CI sets `DOT_TAG=e2e-ci-{pr|nightly|dispatch}`. The `e2e-cli-` prefix on `DEPLOY_TAG` distinguishes our E2E traffic from bulletin-deploy's own. Production health widgets filter cleanly via `!cli.tag:e2e-*`. +- **SAD% propagation** is verified by a regression test in `src/telemetry.test.ts` ("SAD% propagation through transaction envelope"). It confirms `captureWarning` flips `cli.sad="true"` on the root transaction. If it fails, the SAD% dashboard widget will silently degrade to a duplicate of the unexpected-failure rate. ## E2E Tests -- **Local launcher:** `tools/e2e-local.sh [smoke|pr|nightly]` — also callable via `pnpm test:e2e:smoke`, `pnpm test:e2e:pr`, `pnpm test:e2e:nightly`. -- **CI workflow:** `.github/workflows/e2e.yml` — runs on PR / push:main / cron 06:00 UTC / workflow_dispatch. -- **CI matrix:** 13 cells across four matrices — `test-no-publish` (parallel: pr-install, pr-preflight, pr-mod, pr-init-session) + `test-publish` (max-parallel: 1: pr-deploy-frontend, pr-deploy-foundry) + `test-nightly-no-publish` (parallel, schedule/dispatch only: nightly-mod-miss, nightly-diagnostic, nightly-rejections, nightly-chaos-sigint) + `test-nightly-publish` (max-parallel: 1, schedule/dispatch only: nightly-deploy-hardhat, nightly-deploy-multi, nightly-chaos-rpc). Each cell runs a subset via `vitest -t ""`. -- **Release smoke:** `.github/workflows/e2e-release.yml` fires on `release: prereleased`, downloads the `dot-linux-x64` SEA asset, and runs `e2e/cli/published.test.ts` against it. Validates the published binary before stable release. -- **Post-release smoke:** `.github/workflows/e2e-post-release.yml` fires on `release: published` (stable only — `prerelease != true`), waits for the SEA asset, runs `install.sh` (consumer install path via `VERSION= curl … | bash`), then runs `published.test.ts` against the installed `~/.polkadot/bin/dot`. Catches `install.sh` regressions that the prerelease/SEA-download path doesn't. -- **Test files:** `e2e/cli/*.test.ts` (vitest, spawned via `bun run src/index.ts`). -- **Reports directory:** `e2e-reports/junit.xml` + `e2e-reports/dot-runs.log` (gitignored). -- **Tag prefix:** `DOT_TAG=e2e-{ci|local}-{trigger}` so Sentry dashboards filter test traffic. The CLI plumbs `DOT_TAG` into the `cli.tag` root-span attribute via `src/telemetry-config.ts`. -- **CI report job name:** `E2E Report` — aggregates per-leg conclusions, posts a sticky PR comment with marker ``, opens an auto-issue on schedule/release fail. -- **Running tests:** see `docs/e2e-running-tests.md` for the full guide — local modes, vitest passthrough, reading results, GitHub triggers, and common operations FAQ. -- **Bootstrap:** see `docs/e2e-bootstrap.md` for the maintainer-facing setup + recovery procedures. The tool itself is `tools/register-e2e-fixtures.ts`. -- **Cleanup cron:** `.github/workflows/e2e-cleanup.yml` runs Sunday 04:00 UTC. Stub today; will sweep rotating moddable state when Phase 5e ships. -- **Design spec:** `docs-internal/2026-05-02-e2e-test-suite-design.md`. +- **Local launcher:** `tools/e2e-local.sh [smoke|pr|nightly]`, also `pnpm test:e2e:{smoke,pr,nightly}`. +- **CI workflow:** `.github/workflows/e2e.yml` — runs on PR / push:main / cron 06:00 UTC / workflow_dispatch. 13 cells across four matrices (`test-no-publish`, `test-publish`, `test-nightly-no-publish`, `test-nightly-publish`); publish legs run `max-parallel: 1` to avoid stomping a shared deployer account. +- **Release smoke:** `.github/workflows/e2e-release.yml` (on `release: prereleased`) and `.github/workflows/e2e-post-release.yml` (on `release: published`) run `published.test.ts` against the SEA asset and the `install.sh` consumer path respectively. +- **Test files:** `e2e/cli/*.test.ts`. Reports: `e2e-reports/junit.xml` + `e2e-reports/dot-runs.log` (gitignored). CI report job is `E2E Report` — sticky PR comment marker ``. +- **Guides:** `docs/e2e-running-tests.md` (running + reading), `docs/e2e-bootstrap.md` (maintainer setup), design spec at `docs-internal/2026-05-02-e2e-test-suite-design.md`. --- # Product context: playground.dot -Source: Playground Full Spec v0.12, May 2026. TL: Ionut. PM: Rebecca. Team: Charles, Utkarsh, Todor, Reinhard, Sveta (Designer), Karim (Dept Head), RevX team (parallel workstream). LGTMs: Karim / Gav / Pierre. Kanban: https://github.com/orgs/paritytech/projects/278. The summary below captures the mechanics that affect frontend / contract / CLI decisions. - -## What playground.dot does, end-to-end +Source: Playground Full Spec v0.12, May 2026. Team: Ionut (TL), Rebecca (PM), Charles, Utkarsh, Todor, Reinhard, Sveta (Design), Karim (Dept), RevX team (parallel). Kanban: https://github.com/orgs/paritytech/projects/278. -playground.dot is a mobile-first quest platform for the Web3 Summit Developer Lab (18–19 June 2026, Berlin). A developer arrives at the venue, scans a QR or visits the URL, picks a tutorial or sample app, mods it with AI assistance, and deploys their own version live on Polkadot Hub — target time-to-deploy is about thirty minutes from a cold start, with no prior Polkadot experience. +## What it is -The "definition of done" for V1 is exactly that loop: open → tutorial → live deployed app on Polkadot Hub, in about thirty minutes, by a developer who's never touched Polkadot before. The app must be reliable and performant. +playground.dot is a mobile-first quest platform for the Web3 Summit Developer Lab (18–19 June 2026, Berlin). A developer scans a QR or visits the URL, picks a tutorial or sample app, mods it with AI help, and deploys their own version live on Polkadot Hub — target time-to-deploy is ~30 minutes from a cold start, with no prior Polkadot experience. -**V1 is the only active build target.** V2 and beyond are directional ideas — do not implement unless a specific issue or PR explicitly requests it. +**V1 is the only active build target.** V2+ are directional ideas — do not implement unless an issue or PR explicitly requests it. -## App structure: three tabs +## App structure -The playground-app has three tabs (not a single "registry browser"). All three are V1 scope: +Three tabs in the playground-app (not a single "registry browser"). All three are V1 scope: | Tab | Purpose | |---|---| -| **Playground** | Quest-forward onboarding. Tutorial hero, sample apps, how it works, ideas to try, leaderboard | -| **Apps** | Registry browser. All deployed apps, search, category filters, sort options, featured section | -| **Profile** | Personal hub. Deployed apps, starred apps, rank, storage info, name | +| **Playground** | Quest-forward onboarding. Tutorial hero, sample apps, how it works, leaderboard. | +| **Apps** | Registry browser. All deployed apps, search, category filters, sort, featured section. | +| **Profile** | Personal hub. Deployed apps, starred apps, rank, storage info, name. | -**Tab naming:** the registry tab is **"Apps"** — **not** "dAppStore", "store", or "dApp store". Pinning badge is **"Pinned"** — **not** "Staff pick". +**Vocabulary:** the registry tab is "Apps" — never "dAppStore" / "store" / "dApp store". Pinning badge is "Pinned" — never "Staff pick". ## Key repositories -| Repo | URL | Purpose | -|---|---|---| -| Playground app | https://github.com/paritytech/playground-app | Registry browser + Playground tab + Profile | -| Playground CLI (this repo) | https://github.com/paritytech/playground-cli | DOT CLI | -| Tutorial (The Stadium / RPS) | https://github.com/paritytech/Rock-Paper-Scissors | Rock Paper Scissors, 4 levels | -| Empty/starter template | https://github.com/paritytech/playground-app-template | Blank-canvas starter | -| Product SDK (Parity) | https://github.com/paritytech/product-sdk | Publishes `@parity/product-sdk-*` | -| triangle-js-sdks (Nova Spektr) | https://github.com/paritytech/triangle-js-sdks | Publishes `@novasamatech/host-api` + `@novasamatech/product-sdk` (TrUAPI low-level transport) — separate from the Parity Product SDK | -| Attestation Protocol | https://github.com/paritytech/attestation-protocol | Polkadot Attestation Protocol — used for stars/ratings in V2 | +| Repo | Role | +|---|---| +| `paritytech/playground-app` | Registry + Playground tab + Profile | +| `paritytech/playground-cli` (this repo) | `dot` CLI | +| `paritytech/Rock-Paper-Scissors` | The Stadium tutorial (4 levels) | +| `paritytech/playground-app-template` | Blank-canvas starter | +| `paritytech/product-sdk` | Publishes `@parity/product-sdk-*` | +| `paritytech/triangle-js-sdks` | Publishes `@novasamatech/host-api` + `@novasamatech/product-sdk` (TrUAPI) — separate from the Parity SDK | +| `paritytech/attestation-protocol` | Used for stars/ratings in V2 | ## How the pieces fit together -This CLI is one of several components. The user-visible flow stitches together other components owned by other teams: - -| Component | Owned by | Role in the flow | +| Component | Owned by | Role | |---|---|---| -| **playground-app** | Frontend / contract team | Three tabs (Playground / Apps / Profile), App Detail Page, publish pipeline | -| **DOT CLI** (this repo, `dot` binary) | CLI team | Local IDE path: `dot init`, `dot mod`, `dot build`, `dot deploy --playground`, `dot logout`, `dot update` | -| **RevX** | Leo / RevX team | Browser IDE; opens via deep-link `revx.dev/editor?mod=` (no quest param — level picker lives inside RevX) | -| **Tutorial** | Todor | The single structured tutorial (Decentralised Rock, Paper, Scissors, 4 levels, ~30 min) — separate template repo | -| **Sample apps** (~4 for V1, ≥10 for V2) | Various, TBC | Each is its own repo with `setup.sh`, `.claude/skills/`. The Ballot is the first. **No `quests.json`** — quest ideas live in the README | -| **`@parity/product-sdk-*` (Product SDK)** | Parity platform team | All chain interactions go through these packages. Parity-maintained, dapp-facing. Depends at runtime on Nova Spektr's `@novasamatech/host-api` + `@novasamatech/product-sdk` (TrUAPI), published from `paritytech/triangle-js-sdks` — separate Nova project, not a rebrand. This CLI is fully on `@parity/product-sdk-*`; `@polkadot-apps/*` is gone (see Non-obvious invariants for guard details) | -| **Bulletin Chain** | Bulletin / infra | Decentralised storage for app metadata, icons, frontend assets. Mainnet live 7 May 2026 | -| **DotNS** | DotNS team | `.dot` domain reservation during publish | -| **Polkadot app + PoP** | Mobile app team / Gav | Sign-in via QR scan; provisions session keys; PoUD/PoP enable PGAS claims | +| **playground-app** | Frontend/contract team | Three tabs, App Detail Page, publish pipeline | +| **`dot` CLI** (this repo) | CLI team | Local IDE path: `dot init`, `dot mod`, `dot build`, `dot deploy --playground`, `dot logout`, `dot update` | +| **RevX** | Leo / RevX | Browser IDE; opens via `revx.dev/editor?mod=` (no quest param) | +| **Tutorial** | Todor | The Stadium (RPS, 4 levels, ~30 min) | +| **Sample apps** (~4 for V1, ≥10 for V2) | Various | Each is its own repo with `setup.sh` + `.claude/skills/`. Quest ideas live in the README — no `quests.json`. The Ballot is the first confirmed sample. | +| **`@parity/product-sdk-*`** | Parity platform | All chain interactions. Depends on Nova Spektr's `@novasamatech/host-api` + `@novasamatech/product-sdk` (TrUAPI). | +| **Bulletin Chain** | Bulletin/infra | Decentralised storage for app metadata, icons, assets. Mainnet live 7 May 2026. | +| **DotNS** | DotNS team | `.dot` domain reservation during publish. | +| **Polkadot app + PoP** | Mobile / Gav | Sign-in via QR; provisions session keys; PoUD/PoP enable PGAS claims. | ## Network -**Current network:** Paseo Next v2 (`ACTIVE_TESTNET_ENV = "paseo-next-v2"` in `src/config.ts`). The earlier PreviewNet stop has been completed; the three-stage transition language is retired. +**Current:** Paseo Next v2 (`ACTIVE_TESTNET_ENV = "paseo-next-v2"` in `src/config.ts`). -**Summit network:** the event itself runs on a **Summit-specific closed devnet** operated by Parity. All participants get pre-allocated allowances — **no storage or PGAS constraints during the event**. The devnet switches off at the closing ceremony and apps cease to exist. Communicate this clearly in pre-event comms, deploy flow, and on the day. "Save your repo to GitHub" is the consistent message throughout. +**Summit network:** the event itself runs on a **Summit-specific closed devnet** operated by Parity. All participants get pre-allocated allowances — **no storage or PGAS constraints during the event**. The devnet switches off at the closing ceremony and apps cease to exist. "Save your repo to GitHub" is the consistent message. **Don't hardcode "Paseo Next v2" as the permanent network** — the Summit devnet is a separate deployment, gated by `src/config.ts::CONFIGS`. -**Don't hardcode** "PreviewNet" or "Paseo Next v2" as the permanent network — the Summit devnet is a separate deployment. The chain-config-in-one-file invariant (`src/config.ts::CONFIGS`) keeps the Summit devnet swap to a single switch. - -**Storage in production** (outside the Summit devnet): Bulletin storage is time-limited and requires renewal. Frame this as a feature, not a limitation — time-bound deployments encourage active curation, and renewal is how you signal an app is still worth keeping alive. +**Production storage** (outside the Summit devnet): Bulletin storage is time-limited and requires renewal. Frame this as a feature — time-bound deployments encourage active curation. ## PoP auth + session key model -Sign-in is **never** described as "wallet" in the product — it's an **account**. The flow: +Sign-in is **never** described as "wallet" in the product — it's an **account**. 1. User taps sign-in → desktop shows a QR; mobile triggers the Polkadot app directly. -2. Scanning authenticates via PoP (Proof of Personhood) and creates a **session key** locally. -3. The session key is **pre-loaded** via a single `host_request_resource_allocation([BulletinAllowance, StatementStoreAllowance, SmartContractAllowance])` call: one authorisation dialog, then the session flows without interruption. -4. From that point until logout, the publish flow + on-chain interactions are signed by the session key. The user is never asked to top up, fund, or manually acquire tokens. - -A brief QR scan explanation is shown before sign-in: "You'll need the Polkadot App on your phone — this is how you prove you're a real person." +2. Scanning authenticates via PoP and creates a session key locally. +3. The session key is pre-loaded via a single `host_request_resource_allocation([BulletinAllowance, StatementStoreAllowance, SmartContractAllowance])` call: one authorisation dialog, then the session flows. +4. From then until logout, publish + on-chain interactions are signed by the session key. The user is never asked to top up, fund, or acquire tokens. -`dot logout` (CLI) signs out, notifies the mobile app, and cleans up the local session. +`dot logout` signs out, notifies the mobile app, and clears the local session. -The CLI must not present fee-acquisition UX — the session key model means fees are invisible to the user. If you find yourself designing a "buy tokens" or "top up" flow, something has gone wrong upstream. - -**Session keys confirmed KEEP for Summit.** Reasons: without them every on-chain action needs phone approval (3–5+ per publish); mobile signing has had reliability issues; batching breaks PGAS; the RevX browser path needs them for signing without constant phone round-trips. +**The CLI must not present fee-acquisition UX.** If you find yourself designing a "buy tokens" or "top up" flow, something has gone wrong upstream. Session keys are confirmed kept for Summit — without them every action needs phone approval and batching breaks PGAS. ## PGAS and fees -**PGAS (People Gas)** is a burnable sufficient asset on Asset Hub that covers all playground on-chain actions — DotNS registration, registry calls, contract deploys, star/unstar, visibility toggle. Claimed via a ZK ring-VRF proof of personhood — privacy-preserving, sybil-resistant, no prior token ownership required. +**PGAS (People Gas)** is a burnable sufficient asset on Asset Hub covering all playground on-chain actions. Claimed via ZK ring-VRF PoP — privacy-preserving, sybil-resistant, no prior token ownership. -**Confirmed values (PR #880, merged 4 May 2026):** - Lite PoP / PoUD: 40 claims/day × 0.005 DOT = 0.2 DOT/day - Full PoP: 100 claims/day × 0.005 DOT = 0.5 DOT/day -- PGAS pegged 1:1 to DOT for fee payment - -**Budget is sufficient:** ~180–200 transactions across 2 days for an active developer needs ~0.2 DOT — comfortably inside the Lite PoP 2-day budget (0.4 DOT). - -**PoUD → PGAS flow:** downloading the Polkadot App automatically grants PoUD → can claim PGAS via the mobile app. `host_request_resource_allocation([SmartContractAllowance])` at session start → phone submits v5 claim → PGAS in product account → all transactions paid automatically. +- PGAS pegged 1:1 to DOT for fees -**Claim path vs spend path:** PGAS claiming is **v5 extrinsic only** — the mobile app handles it, not the CLI/Product SDK. Spending PGAS is v4 and works everywhere. **Batching transactions breaks PGAS fee payment** — the publish flow must remain as sequential individual transactions. +Budget is sufficient for ~180–200 transactions across 2 days. PGAS claim path is **v5 extrinsic only** (mobile-only); spending PGAS is v4 and works everywhere. **Batching transactions breaks PGAS fee payment** — the publish flow must remain sequential individual transactions. -**Summit devnet:** allowances are pre-allocated. PGAS and storage constraints are **not operational concerns during the event**. Vouchers, soft-limit messaging, Bulletin expiry countdown UI, and `dot voucher` are all **removed from V1**. Don't reintroduce. +Summit devnet allowances are pre-allocated. Vouchers, soft-limit messaging, Bulletin expiry countdown UI, and `dot voucher` are all **removed from V1** — don't reintroduce. ## The publish flow (5 steps, all paid by the session key) @@ -200,149 +188,77 @@ The CLI must not present fee-acquisition UX — the session key model means fees | 2 | Reserve `.dot` domain on Polkadot Hub | "Registering your .dot domain..." | | 3 | Register on the playground registry | "Publishing to playground registry..." | | 4 | Link app to user account | "Linking to your account..." | -| 5 | Share — generate a shareable link | "Your app is live!" | +| 5 | Generate a shareable link | "Your app is live!" | -Per-step plain English error messages — never hex revert codes. Retries are safe: Bulletin uploads deduplicate by content, DotNS skips if already owned, registry updates existing entry. Re-deploys show "Updating myapp.dot" not "Publishing myapp.dot". +Plain-English error messages — never hex revert codes. Retries are safe: Bulletin uploads dedupe by content, DotNS skips if already owned, registry updates existing entry. Re-deploys show "Updating myapp.dot", not "Publishing myapp.dot". Account switch mid-publish aborts with `Account changed mid-publish — please re-run from the new account`. -**Account switch during publish:** if user switches accounts mid-publish, abort with `Account changed mid-publish — please re-run from the new account`. +**Publish validation (V1):** domain uniqueness (DotNS contract, first on-chain tx wins) and required fields. Image format/size limits deferred to V2. -**Publish validation (V1):** domain uniqueness (enforced at the DotNS contract level — first on-chain transaction wins) and required fields (domain, metadata). **Image format/size limits deferred to V2.** +**Post-deploy CLI output target:** live URL (`yourapp.dot.li`) + playground detail link + share CTA ("Share your app — let others mod it") + sovereignty line ("Your app is live on Bulletin Chain, registered on Polkadot Hub, accessible at yourapp.dot.li. Nobody controls this but you.") + name reveal ("You're live as swift-cosmic-builder. Change your name in playground.dot → My Profile.") + moddable nudge + docs link. -**Post-deploy CLI output (V1 target):** -- Live URL (`yourapp.dot.li`) as clickable deep link -- Playground detail page link (`playground.dot/app/yourapp.dot`) -- **Primary CTA:** "Share your app — let others mod it" → copies the playground detail page link -- **Sovereignty line:** "Your app is live on Bulletin Chain, registered on Polkadot Hub, accessible at yourapp.dot.li. Nobody controls this but you." -- **Name reveal:** "You're live as swift-cosmic-builder. Change your name in playground.dot → My Profile." -- **Moddable nudge:** "Make your app moddable — connect your GitHub repo so others can build on your work, and so you keep your code after the Summit ends." -- **Docs link:** "Learn more about building on Polkadot → [docs link]" +## Content tiers -**Star prompt after mod deploy moved to Stretch** in v0.12 — was V1, now deferred. Not shown in V1 CLI/RevX. +Three tiers share the same contract; the frontend differentiates via pinning + App Detail Page variant. -## Content tiers in the registry - -Three tiers all live in the same contract; the frontend differentiates them via pinning + App Detail Page variant. - -**Tier 1 — The Stadium tutorial.** One repo (https://github.com/paritytech/Rock-Paper-Scissors), one app entry, pinned. +**Tier 1 — The Stadium tutorial.** Single repo (`paritytech/Rock-Paper-Scissors`), one app entry, pinned. | Level | Name | Scope | XP | Mobile | |---|---|---|---|---| | 1 | Local Challenger | Mod UI/theming. No contract changes | 25 | ✅ | -| 2 | On-Chain Record | Save game results to Bulletin | 25 | Possibly via RevX (no contracts) — pending RevX-mobile confirmation | +| 2 | On-Chain Record | Save game results to Bulletin | 25 | Maybe via RevX (pending) | | 3 | The Leaderboard | Deploy leaderboard smart contract | 25 | ❌ laptop required | -| 4 | Multiplayer | P2P via Statement Store. Challenge via link/QR | 25 | ❌ laptop required | - -Total **100 XP** across the four levels. - -**Fixes still pending in the RPS repo's `quests.json`:** XP shows 50/100/150/200 = 500 — must be 25/25/25/25 = 100. Tutorial time shown 90m — must be ~30 min. Confirm both with Todor. - -**Tier 2 — Sample apps (~4 for V1, ≥10 for V2).** Each is its own repo, pinned. **No `quests.json`** — quest **ideas** live in the README. 10 XP awarded per first deploy per new domain. Re-deploys to the same domain don't re-award. - -**Candidate sample apps** (full list — Rebecca actively commissioning; The Ballot is confirmed V1): +| 4 | Multiplayer | P2P via Statement Store | 25 | ❌ laptop required | -| App | Description | Key Polkadot stack | Verticals | -|---|---|---|---| -| **The Ballot** *(confirmed V1)* | PoP-gated polling | Smart contracts, PoP | governance / social | -| Dot.link | Decentralised link-in-bio on .dot | Bulletin, DotNS | personal / identity | -| Kudos | Permanent signed peer recognition | Smart contracts, PoP, account-to-account | social / professional | -| Countdown | Unstoppable event countdown tied to block height | Bulletin, DotNS, block timing | personal / creative | -| Proof Board | Signed permanent statements | Bulletin, Statement Store, PoP | social / censorship-resistance | -| Shout | Anonymous PoP-verified message board | PoP anonymity, Statement Store | social / identity | -| Pact | Public on-chain promise between two PoP accounts | Smart contracts | social / games | -| Signal | Anonymous human-verified survey via ZK PoP | ZK PoP, Statement Store | governance / identity | -| Squads | On-chain group formation with PoP membership | Smart contracts, PoP, multi-account | social / community | -| Collab | Shared docs with signed attributed edits | Smart contracts, Bulletin, PoP | productivity / creative | -| Timelock | Message sealed until future block height | Smart contracts, block timing | games / creative | -| Chronicle | Personal blog on .dot signed with PoP | Bulletin, DotNS, PoP | personal / creative | -| Minimarket | Decentralised classifieds on .dot | Bulletin, DotNS, Statement Store | commerce / social | -| Flipside | Two-sided PoP-gated debate/vote | Smart contracts, PoP voting | governance / community | -| Reputation | Mutual PoP endorsements (attestation) | Attestation protocol, PoP | professional / identity | +Total **100 XP**. Outstanding RPS-repo fix: `quests.json` shows 50/100/150/200 = 500 XP; must be 25/25/25/25. Confirm with Todor. -**Recommended V1 priorities (fastest to build):** Dot.link, Kudos, Countdown, Proof Board — alongside The Ballot. +**Tier 2 — Sample apps** (~4 V1, ≥10 V2). Each is its own repo, pinned. **No `quests.json`** — quest ideas live in the README. 10 XP per first deploy per new domain. Confirmed V1: **The Ballot** (PoP-gated polling). Recommended V1 priorities (fastest to build): Dot.link, Kudos, Countdown, Proof Board. Full candidate list maintained by Rebecca. -**Sample app spec (V1):** -- Start from `playground-app-template` — not from scratch. Wait for app#101 (generic Product SDK skills in template) to land before commissioning. -- Required files: **README** (what it does, what makes it interesting, quest ideas, SDK packages used, key files, "what you just built" explanation), `setup.sh` (idempotent, prints `[setup] doing X...`, fails with actionable errors), `.claude/skills/app-context.md` (~10 lines), generic Product SDK skills via app#101. -- **No `quests.json` for sample apps.** -- Must be moddable (public GitHub repo required). Naming convention: `sample-[appname]-app`. -- **Size limit: one Bulletin chunk (~10MB, TBC with Bulletin team).** Compress images, slim the bundle. -- Use at least one element of the Polkadot product stack beyond DotNS. -- Must work on the active V1 network. +Sample app spec: start from `playground-app-template`, ship a README (quest ideas + SDK packages + key files), idempotent `setup.sh`, `.claude/skills/app-context.md` (~10 lines). Must be moddable (public GitHub). Size limit: one Bulletin chunk (~10 MB, TBC). Naming `sample--app`. -**Tier 3 — Participant apps.** Everything modded and deployed by Summit attendees, growing throughout the event. Shown below pinned items. +**Tier 3 — Participant apps.** Everything modded and deployed by Summit attendees, growing through the event. Shown below pinned items. -**Empty/starter template** (https://github.com/paritytech/playground-app-template) is **pinned alongside** the tutorial and sample apps for blank-canvas builds. +The empty/starter template (`paritytech/playground-app-template`) is **pinned alongside** the tutorial and sample apps for blank-canvas builds. ## XP and stars -Two separate concepts that are easy to conflate. Points are renamed to **XP** throughout V1 (aligns with Sveta's design). +Points are renamed to **XP** throughout V1 (aligns with design). -**XP = leaderboard score (Top Builders).** Stored on-chain as a per-account running balance — consistent across all devices and venue screens in real time. XP only ever goes up. +**XP = leaderboard score (Top Builders).** Stored on-chain as a per-account running balance. XP only goes up. | Action | XP | Notes | |---|---|---| -| Tutorial level completed | 25 | Max 100 total. Once per `(account, track_id, quest_id)` — prevents farming by redeploying same level to different domains | -| New app deployed | 10 | First deploy per domain only. Re-deploys to same domain = update, no additional XP | -| Star received | 10 | Per star awarded to your app | -| **Someone mods your app** | **25** | **New in v0.12** — strongest signal, effort-based endorsement. Tracked via `mod_count` | +| Tutorial level completed | 25 | Max 100. Once per `(account, track_id, quest_id)`. | +| New app deployed | 10 | First deploy per domain only. | +| Star received | 10 | Per star awarded to your app. | +| Someone mods your app | 25 | New in v0.12 — strongest signal. Tracked via `mod_count`. | -Sample apps award 10 XP per first deploy per domain only — **no quest-based XP**. Quest ideas in the README are inspiration, not a scoring mechanism. +**Stars = what users award.** Cumulative total displayed (never average X.X / 5), one-way (can upgrade but cannot remove), self-starring forbidden at contract level, unlimited per user. Each star earns the owner +10 XP. **Binary vs max-2 stars decision pending.** -**Stars = what users *award* to other apps.** -- **Binary vs max-2-stars decision pending this week.** Either way: cumulative total displayed (never as average X.X / 5), **one-way** (for 2-star: can update 1→2 but cannot remove), self-starring forbidden at contract level, **unlimited** (no per-user allocation cap — that "Stars to give: N" pattern is explicitly rejected as engagement-killer). -- Each star earns the app's owner +10 XP. -- Stars also serve as personal favourites. - -**Leaderboard (now V2 in v0.12 — was V1):** the **Top Builders by XP** leaderboard UI moved from V1 to V2 in the latest spec. The underlying on-chain XP balance is still V1 — venue screens can read it directly. "Most modded" and "most starred" **sort options on the Apps grid also moved to V2**. - -**Tutorial completion verification (V1):** XP awarded on deploy automatically. For prize purposes (~$2k prize pool), event admins manually verify top-leaderboard participants completed the tutorial before awarding. +Leaderboard UI and "most modded" / "most starred" sort options moved from V1 to V2 in v0.12. The on-chain XP balance is still V1 — venue screens can read it directly. ## RevX deep-link contract -`revx.dev/editor?mod=` +`revx.dev/editor?mod=` — `mod=` required, **no `&quest=` param** (level picker lives inside RevX). Single "Open in RevX" button per app, same for tutorial / sample / participant apps. -- `mod=` — required. The .dot domain of the source app to clone. -- **No `&quest=` param.** Level/quest picker happens **inside RevX**. **Single "Open in RevX" button per app** — applies to tutorial, sample apps, and participant apps alike. +RevX downloads source as HTTPS tarball (same as the CLI). After load: PoP auth (QR on desktop, direct on mobile), AI chat pre-loaded with the template's `CLAUDE.md` + Product SDK skills, CLI bridge maps RevX UI actions to `dot build`, `dot deploy --playground`. -RevX downloads the source as an HTTPS tarball — same as the CLI — so no git or `gh` is required to start. After load: PoP auth (QR on desktop, direct on mobile), AI chat pre-loaded with the template's `CLAUDE.md` + Product SDK skills, and a CLI bridge that maps RevX UI actions to `dot build`, `dot deploy --playground`. +⚠️ **Web container constraint:** the RevX browser web container is Node/TS/JS only — cannot run the IPFS binary. The CLI's Kubo-binary path (see `jsMerkle: false` invariant) blocks RevX's main storage upload until bulletin-deploy's pure-JS merkleizer is fixed. -⚠️ **Web container constraint:** the RevX browser web container is Node/TS/JS only — cannot run the IPFS binary. The CLI's current Kubo-binary path (see invariant on `jsMerkle: false`) is the constraint here — until bulletin-deploy's pure-JS merkleizer is fixed, RevX's main storage upload story is blocked. - -GitHub login in RevX is currently deactivated pending security review. Without it, apps deployed from RevX are non-moddable by others (the source URL isn't published). +GitHub login in RevX is currently deactivated pending security review. ## CLI deep-link contract (`dot mod`) -The CLI's `dot mod` command downloads the source as an **HTTPS tarball** — no git, no `gh`, no clone. Forms: - -- Interactive picker: `dot mod` (lists moddable apps only) -- Direct: `dot mod ` - -After download, `setup.sh` runs and its output is kept visible/logged. `dot mod` also writes the source domain to a local metadata file — passed at deploy time so the registry can store "Modded from: [domain]". **Modded-from metadata capture is not yet built** (V1 P0). - -Subsequent commands: `dot build` (auto-detects Rust/Solidity/EVM contracts + frontend, picks the package manager, installs if missing), `dot deploy --playground` (full 5-step pipeline, **should default to moddable** — current code defaults non-moddable, needs fixing). - -`dot init` covers first-time setup. Dependencies install in parallel: the Rust chain (rustup → Rust nightly → rust-src → cdm) is sequential due to hard dependencies, but IPFS, foundry, and git run concurrently. Estimated saving: ~3 minutes on a fresh machine — not yet built. Then: PoP QR auth, session key creation. **No voucher prompt** — vouchers are removed from V1. +`dot mod` downloads source as an HTTPS tarball — no git, no `gh`, no clone. Forms: `dot mod` (interactive picker over moddable apps), `dot mod ` (direct). After download, `setup.sh` runs and stays visible/logged. `dot mod` writes the source domain to local metadata, passed at deploy time so the registry can store "Modded from: [domain]". *Modded-from metadata capture is not yet built (V1 P0).* -**CLI command naming open question:** `dot x` vs `play-dot x` vs `playdot x` — decision needed before on-site materials are printed. +Subsequent commands: `dot build` (auto-detects Rust/Solidity/EVM + frontend, picks the package manager), `dot deploy --playground` (full 5-step pipeline — **should default to moddable**; current code defaults non-moddable, needs fixing). ## Moddable default flow -`dot deploy --playground` **should default to moddable** (current code defaults non-moddable — bug, needs fixing). Full guided flow (spec-level UX intent): +`dot deploy --playground` **should default to moddable** (current code defaults non-moddable — bug). The spec-level intent is to read an existing public GitHub origin, deploy moddable automatically, and prompt only if missing. **The CLI itself never invokes `gh`** (see invariants above) — that's the playground-app's job, not the CLI's. Non-moddable apps still get DotNS + Bulletin links; they just can't be cloned. -1. CLI checks `gh` auth + existing public repo. -2. **Repo found** → deploy as moddable automatically. -3. **No repo** → prompt: "Make your app moddable so others can build on it? (recommended) [Y/n] — requires GitHub". -4. If Y → "This needs a GitHub repo. Want us to create one for you? (requires gh CLI installed and logged in) [Y/n]". If Y → `gh auth login` if needed → user-initiated repo create → push → deploy as moddable. +## quests.json (tutorial only) -**Important CLI invariant** (already enforced — see `src/utils/deploy/moddable.ts`): the CLI **never invokes `gh`**. `resolveRepositoryUrl()` reads existing `origin`, validates it's a public GitHub URL, and records it in metadata. There is no auto-create path; missing `origin` / private repos / non-GitHub URLs all hard-fail with actionable messages. The spec's "guided flow" above describes the intended UX in playground-app — the CLI's contract is stricter and stays user-initiated. - -GitHub login is **NOT required** to deploy. Non-moddable apps still get DotNS + Bulletin links — they just can't be cloned by others. - -## quests.json shape (tutorial only) - -In v0.12 **only the tutorial** ships a `quests.json`. Sample apps no longer have one — quest ideas live in the README. - -**Schema:** +In v0.12 only the tutorial ships a `quests.json`. Sample apps don't. ```json { @@ -369,119 +285,49 @@ In v0.12 **only the tutorial** ships a `quests.json`. Sample apps no longer have } ``` -The tutorial repo also ships a `setup.sh` and a `.claude/skills/` directory. **app#101 (In Progress)** copies the generic Product SDK skills into all templates and sample apps automatically. - -## Product SDK packages - -The product treats this as Polkadot's equivalent of viem + wagmi. All chain interactions go through these. **Two distinct repos to keep straight:** -- **`paritytech/product-sdk`** (private) publishes `@parity/product-sdk-*` (signer, contracts, bulletin, chain-client, tx, keys, host, storage, statement-store, address, descriptors, terminal, logger, utils, etc.). Parity-maintained, dapp-facing, supersedes `@polkadot-apps/*`. **This CLI is fully migrated** to product-sdk — see the Non-obvious invariants section for the CI guard that prevents `@polkadot-apps/*` re-imports, the caret-range pin model, and load-bearing overrides. -- **`paritytech/triangle-js-sdks`** (public POC) publishes `@novasamatech/host-api` and `@novasamatech/product-sdk` (TrUAPI low-level transport). **Not** a rebrand of the Parity SDK — a separate Nova-Spektr project. +The tutorial repo also ships `setup.sh` + `.claude/skills/`. app#101 propagates generic Product SDK skills into all templates and sample apps automatically. -TrUAPI v0.3 — changes TBC. Watch for breaking changes. +## V1 CLI feature scope -## V1 feature scope (CLI-relevant) +P0 / P1: +- `dot init` — first-time setup, QR auth, session key, dependency install. Parallelised dep install (~3 min saving) not yet built. +- `dot mod` — HTTPS tarball, interactive picker, source-domain capture (not yet built, V1 P0). +- `dot build` — auto-detect Rust/Solidity/EVM + frontend. +- `dot deploy --playground` — full 5-step pipeline; default-moddable bug fix outstanding. +- `dot logout`, `dot update` (works via npm for RevX). +- Plain-English error messages for all common on-chain failures (replacement tables in playground-app's CLAUDE.md / spec). -CLI / DevX features that are P0 / P1 for V1: - -- `dot init` — first-time setup, QR auth, session key, dependency install -- `dot init` — parallelised dependency install (IPFS/foundry/git parallel to Rust chain — ~3 min saving). Not yet built. -- `dot mod` — HTTPS tarball download (no git/gh required), interactive picker, source-domain capture for modded-from metadata -- `dot build` — auto-detects Rust/Solidity/EVM contracts + frontend -- `dot deploy --playground` — full 5-step pipeline; **must default to moddable** (bug — current default is non-moddable) -- `dot logout` -- `dot update` (works via npm for RevX) -- Plain English error messages for all common on-chain failures (see UI Copy section in the spec for the full replacement table — covers `--moddable` errors, DotNS validation, mobile signing, funder exhaustion, resource allocation, cdm/forge build failures, etc.) -- Modded-from metadata capture in CLI (`dot mod` writes source domain) — not yet built, V1 P0 - -**Removed from V1 (do not reintroduce):** -- `dot voucher ` command -- Conditional voucher prompt at `dot init` -- Soft-limit communication -- Bulletin expiry countdown / two-week expiry narrative - -## Go / No-Go criteria (CLI-relevant) - -**Hard blockers** (Summit cannot proceed without these): -1. End-to-end flow: `dot mod` → edit → `dot deploy --playground` → appears in registry (Internal test pass — app#36) -2. RevX path works end-to-end via the CLI bridge: deep-link → auth → edit → deploy → appears in registry -3. The Stadium — all 4 levels deployable -4. Mobile: Level 1 completable end-to-end on phone (Android + iOS) -5. Security review passed — no critical or high findings outstanding -6. Internal 30-minute test pass completed (app#36) -7. Summit devnet confirmed operational and stable - -## Directional ideas (V2 / V2.5 / Stretch) — CLI-relevant items - -- **V2:** `dot preview` (local preview before deploy — equivalent to `npm run dev`), lazy dependency installation (only install what current level needs), `dot deploy` defaulting to `--playground` (open question), CDM → cargo-pvm migration (Charles owns). -- **Stretch:** star prompt after mod deploy (moved from V1). - -## Out of scope (per spec) - -- Building from scratch (entry is always tutorial / sample app / empty starter) -- Multiple tutorial tracks (The Stadium is the only one) -- DeFi quests (regulatory) -- Permanent deletion by owners (visibility toggle only) -- Account creation outside the Polkadot app / PoP flow -- Contract-modding on mobile (Level 1 / UI-only quests on phone) -- Chat Extensions sharing (descoped) -- **Vouchers / `dot voucher` / soft-limit messaging / Bulletin expiry countdown UI** — all removed in v0.12 -- **Account status component (#67)** — parked, confirmed intentional given the devnet -- DOT airdrop as a W3S mechanism (stale) +**Removed from V1, do not reintroduce:** `dot voucher`, conditional voucher prompt at `dot init`, soft-limit communication, Bulletin expiry countdown / two-week expiry narrative. ## Vocabulary the product uses -The product is consistent about its language. CLI output, error messages, and command names should follow: +CLI output, error messages, and command names should follow: | Concept | Term used | Avoided | |---|---|---| -| Taking on a challenge | accept a quest / join a quest | "try", "attempt", "do" | -| Modifying an app | mod (verb and noun) | "remix", "fork", "clone" | -| The modified version | your mod / your app | "your fork", "your remix" | -| Full deploy + publish | `dot deploy --playground` | "dot ship" | -| Publishing to the registry | deploy / publish | "submit", "upload", "release" | -| The structured tutorial | tutorial / The Stadium | "tutorial track", "tutorial quest" | -| Open-ended modding challenge | quest idea | "hackathon", "challenge" | -| Working apps with quest ideas | sample apps | "templates", "starter apps" | -| User identity | account | "wallet" | -| Deployment network | Polkadot Hub | "mainnet" (sparingly), "Paseo" never in user-facing copy | -| Host ↔ product transport layer | TrUAPI | "TruAPI", "Host API", "triangle-js-sdk", "host-api" | -| App others can mod | **moddable** (two d's) | "modable" (one d — wrong) | -| Leaderboard score | **XP** (renamed from "points") | "points" (legacy term) | - -**Plain English error messages:** the spec includes complete replacement tables for **all** current CLI error strings — refer to the UI Copy section in the playground-app CLAUDE.md (or the spec directly) before adding/changing any user-facing CLI error. - -## Timeline (for context) - -| Phase | Target | Scope | -|---|---|---| -| Phase 0 — Foundation | ~~18 Apr 2026~~ (passed) | CLI built. RevX deep-link agreed. Tutorial stubs | -| Phase 1 — V1 Complete | ~~2 May 2026~~ (in progress) | Core flow. ~4 sample apps. Full publish flow. Internal test pass | -| Phase 2 — V1 Audit + V2 Build | 3–16 May 2026 | Internal audit on V1 contract. V2 build in parallel | -| Phase 3 — V2 complete | 17–31 May 2026 | V2 integrated. V2.5 + Stretch only with clear runway | -| June buffer | 1–17 Jun 2026 | Venue setup, demo station prep, dress rehearsal (~9 Jun) | -| Event | 18–19 Jun 2026 | Web3 Summit Developer Lab, Berlin | - -**Hard constraints:** V1 contract freeze ahead of audit. Everything done **31 May 2026**. - -**Testing sessions:** -- 20 May 2026: Session 02 — Deeper Dive (Playground, getlocal, Wire) -- 3 June 2026: Session 03 — Final Regression Check - -## Open questions worth knowing (parking lot) - -- **CLI command naming:** `dot x` vs `play-dot x` vs `playdot x`. Decision needed before Summit. -- **Star system:** binary vs max 2 stars. Decision expected this week. -- **`(account, track_id, quest_id)` uniqueness:** confirm `quest_id` is level-scoped (not domain-scoped) to prevent tutorial farming. -- **TrUAPI v0.3:** what's changing that affects playground? -- **Session key PGAS sizing:** confirmed to cover full 2-day Summit? -- **RevX mobile capability:** does RevX support contract-modding on mobile? Determines Level 2 mobile behaviour. -- **Sample apps V1 list:** The Ballot confirmed. Which 3 others? -- **V2.5:** Option A (social/follow) or Option B (peer verification)? -- **GitHub rate limiting at Summit:** 60 unauthenticated requests/IP/hour. Decision: proxy, mandatory `gh auth`, or accept risk? (CLI already lazy-probes once per `dot mod` to conserve quota — see `runModCommand`.) -- **Session key day 2:** what happens when a developer returns and the session key has expired? Does `dot init` detect and re-initialise? -- **`dot mod` search/filter:** with 50+ apps at Summit, numbered list becomes unwieldy. In scope for V1? -- **quests.json points discrepancy:** confirm 25/25/25/25 = 100 XP with Todor, update RPS repo. -- **Tutorial time:** confirm ~30 min (not 90m in design) with Todor. -- **Sample app size:** confirm one Bulletin chunk = ~10MB with Bulletin team. -- **PGAS batch constraint:** which publish flow transactions are affected? -- **sdk-ink double dry-run calls:** ReviveApi.call + ReviveApi.trace_call doubles rate limit pressure. Monitor and fix before Summit. +| Taking on a challenge | accept a quest / join a quest | try / attempt / do | +| Modifying an app | mod (verb and noun) | remix / fork / clone | +| The modified version | your mod / your app | your fork / your remix | +| Full deploy + publish | `dot deploy --playground` | dot ship | +| Publishing to the registry | deploy / publish | submit / upload / release | +| The structured tutorial | tutorial / The Stadium | tutorial track / tutorial quest | +| Open-ended modding challenge | quest idea | hackathon / challenge | +| Working apps with quest ideas | sample apps | templates / starter apps | +| User identity | account | wallet | +| Deployment network | Polkadot Hub | mainnet (sparingly), Paseo never in user-facing copy | +| Host ↔ product transport layer | TrUAPI | TruAPI / Host API / triangle-js-sdk / host-api | +| App others can mod | **moddable** (two d's) | modable (one d) | +| Leaderboard score | **XP** | points (legacy term) | + +## Out of scope (per spec) + +- Building from scratch (entry is always tutorial / sample app / empty starter). +- Multiple tutorial tracks (The Stadium is the only one). +- DeFi quests (regulatory). +- Permanent deletion by owners (visibility toggle only). +- Account creation outside the Polkadot app / PoP flow. +- Contract-modding on mobile (Level 1 / UI-only quests on phone). +- Chat Extensions sharing. +- Vouchers / `dot voucher` / soft-limit messaging / Bulletin expiry countdown UI. +- Account status component (#67) — parked, intentional given the devnet. +- DOT airdrop as a W3S mechanism. diff --git a/README.md b/README.md index d383eb3..7e2bb2b 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ Flags: - `--private` — publish to the playground with private (owner-only) visibility. Requires `--playground`. Not interactively prompted; pass the flag to opt in. - `--moddable` / `--no-moddable` — publish the source repo URL alongside the deploy so others can `dot mod` it. Requires `--playground`. Interactive prompt (default: no) if omitted. The CLI reads your existing `origin` and records its URL in the Bulletin metadata; it never creates a repo or pushes for you. The deploy fails with an actionable message if `origin` is unset, points to a private repo, or points to anything other than GitHub (since `dot mod` only fetches from `codeload.github.com`). Set up the repo yourself before re-running: create a public repo on GitHub, then `git remote add origin https://github.com//` followed by `git push -u origin main`. (If you happen to have `gh` installed, `gh repo create my-app --public --source=. --push` does both in one shot — `dot` does not require `gh`.) - `--suri ` — override signer with a dev secret URI (e.g. `//Alice`). Useful for CI. -- `--env ` — `testnet` (default) or `mainnet` (not yet supported). +- `--env ` — target environment. Defaults to `paseo-next-v2` (the only one fully wired today). Accepts the bulletin-deploy env IDs (`preview`, `paseo-next`, `paseo-review`, `paseo-next-v2`, `polkadot`, `kusama`) plus the legacy `testnet`/`mainnet` aliases — `testnet` maps to `paseo-next-v2`, `mainnet` to `polkadot`. Any env other than `paseo-next-v2` throws "not supported" until its entry is wired up in `src/config.ts::CONFIGS`. Passing all four of `--signer`, `--domain`, `--buildDir`, and `--playground` runs in fully non-interactive mode. Any absent flag is filled in by the TUI prompt. `--moddable`, `--private`, and `--contracts` are independently optional in both modes — their absence means a non-moddable, public, frontend-only deploy. @@ -216,15 +216,16 @@ The first two are also enforced in CI; running them locally catches the failure ## Dependency Notes - `@parity/product-sdk-*` packages use caret ranges (`^0.x.y`) so upstream patch and minor releases auto-resolve on a fresh `pnpm install`. With pre-1.0 versions, `^` only widens patches within the current 0.x line — a 0.x → 0.(x+1) bump still requires an intentional `package.json` change. CI's `Format` job runs a grep guard that fails the build on any direct `@polkadot-apps/*` import in `src/`, `e2e/`, `scripts/`, or `tools/`. -- `@dotdm/contracts` is pinned to the explicit dev tag `1.1.1-dev.1778274929` — that's the npm publish from the CDM monorepo's `migrate-to-product-sdk` PR (paritytech/contract-dependency-manager#13), the first build that consumes `@parity/product-sdk-*` instead of the legacy `@polkadot-apps/*` line. The `latest` stable (`1.1.1`) still pulls `@polkadot-apps/*` + `polkadot-api@1.x`. Move back to a caret or `latest` once that migrated build is promoted. -- `@novasamatech/*` packages are forced to `0.7.8-2` via `pnpm.overrides`. They come in transitively from `@parity/product-sdk-*` whose `^0.7.7` caret doesn't auto-widen across patches in lockfile updates; the override aligns the tree on the latest Novasama publish. Drop the override once product-sdk widens its own caret. -- `polkadot-api` is pinned to `^2.1.2` and `@polkadot-api/sdk-ink` to `^0.7.0`. The lockfile contains a stale `polkadot-api@1.23.3` only because `@parity/dotns-cli`'s declared dep references it; that CLI ships as a single bundled `dist/cli.js` with all deps inlined, so the 1.x decl is never resolved at runtime. Effectively the runtime is PAPI 2.x-only. -- `bulletin-deploy` is pinned to an explicit version — not `latest`. Currently `0.7.13`. Previously `latest` pointed at 0.6.8 which had a WebSocket heartbeat bug (40s default < 60s chunk timeout) that tore chunk uploads down as `WS halt (3)`; keeping the pin explicit avoids ever sliding back onto that. When bumping, check the release notes for any changes to `deploy()` / `DotNS` APIs we rely on. +- `@dotdm/contracts` is on the `^2.0.x` caret. The 2.0 line is the first to consume `@parity/product-sdk-*` directly; the legacy `1.1.1` stable still pulls `@polkadot-apps/*` + `polkadot-api@1.x` and must NOT be downgraded to. Patch bumps within 2.x are safe. +- `@novasamatech/*` packages are forced to `0.7.9-4` via `pnpm.overrides`. They come in transitively from `@parity/product-sdk-terminal` whose `^0.7.7` caret doesn't auto-widen across patches in lockfile updates; the override aligns the tree on the latest published Novasama line (including RFC-0010 `requestResourceAllocation` on `UserSession`). Drop the override once product-sdk-terminal widens its own caret. +- `polkadot-api` is on `^2.1.x` and `@polkadot-api/sdk-ink` on `^0.7.0`. The lockfile contains a stale `polkadot-api@1.x` only because `@parity/dotns-cli`'s declared dep references it; that CLI ships as a single bundled `dist/cli.js` with all deps inlined, so the 1.x decl is never resolved at runtime. Effectively the runtime is PAPI 2.x-only. +- `bulletin-deploy` is pinned to an explicit version — not `latest`. Currently `0.7.24`. Previously `latest` pointed at 0.6.8 which had a WebSocket heartbeat bug (40s default < 60s chunk timeout) that tore chunk uploads down as `WS halt (3)`; keeping the pin explicit avoids ever sliding back onto that. When bumping, check the release notes for any changes to `deploy()` / `DotNS` APIs we rely on (`jsMerkle`, `signer`, `signerAddress`, `mnemonic`, `rpc`, `attributes`). +- `pnpm.overrides` also redirects `@parity/dotns-cli`'s declared `@polkadot-api/descriptors` dep to `stubs/papi-descriptors-stub/`. `@parity/dotns-cli@0.6.1`'s published manifest references a workspace path (`file:.papi/descriptors`) that doesn't exist in the tarball; pnpm refuses, npm tolerates it. dotns-cli ships as a fully-bundled `dist/cli.js` so the stub (exporting `{}`) is functionally correct. Remove once `@parity/dotns-cli` republishes a clean manifest. ## Architecture Highlights - **Single config module** (`src/config.ts`) — all chain URLs, contract addresses, dapp identifiers and the `testnet`/`mainnet` switch live here. Nothing else in the tree should hard-code an endpoint or address. -- **QR-paired session signer** (`src/utils/auth.ts::createPlaygroundSigner`) — wraps `@parity/product-sdk-terminal@0.2.0+`'s `createSessionSignerForAccount`, which builds a PJS signer with split callbacks under the hood: tx → `session.signPayload` (chain-tx context, no envelope), bytes → `session.signRaw` (mobile applies the `` envelope, correct for arbitrary data). The product account derives from `productId="playground.dot"` + `derivationIndex=0`, which matches what the playground-app uses on the host side so the same address signs in both surfaces. +- **QR-paired session signer** (`src/utils/auth.ts::createPlaygroundSigner`) — wraps `@parity/product-sdk-terminal@0.2.1+`'s `createSessionSignerForAccount`, a PAPI-native signer that routes extrinsic payloads through `session.signRaw({ data: { tag: "Payload", value: hex(toSign) } })` (opaque bytes, no `` envelope) so every signed extension declared by the chain — including paseo-next-v2's `AsPgas` — survives end-to-end. The product account derives from `productId="playground.dot"` + `derivationIndex=0`, which matches what the playground-app uses on the host side so the same address signs in both surfaces. - **Unified signer resolution** (`src/utils/signer.ts`) — one `resolveSigner({ suri? })` call returns a `ResolvedSigner` whether the user is authenticated via QR session or a dev `//Alice`-style URI. Every command threads the result through to its operations instead of branching on source. - **Connection singleton** (`src/utils/connection.ts`) — stores the promise (not the resolved client) so concurrent callers share a single WebSocket. Has a 30s timeout and preserves the underlying error via `Error.cause` for debugging. - **Session lifecycle** (`src/utils/auth.ts`) — `getSessionSigner()` returns an explicit `destroy()` handle. Callers MUST call it (typically from a `useEffect` cleanup) — the host-papp adapter keeps the Node event loop alive. diff --git a/src/commands/init/IdentityLines.tsx b/src/commands/init/IdentityLines.tsx index 4a43b86..0600ae3 100644 --- a/src/commands/init/IdentityLines.tsx +++ b/src/commands/init/IdentityLines.tsx @@ -22,13 +22,15 @@ import { productAccountDisplay } from "./identityLine.js"; * Two-line identity block shown after a successful login: * * username alice.dot - * product account 5Grwva...utQY (0x1a2b...ef34) + * product account () * - * The username lookup is async (queries People parachain) and has a 5s - * timeout inside `lookupUsername`; the product account is synchronous (pure - * sr25519 soft derivation). A `(looking up...)` placeholder renders while - * the lookup is in flight; failures and missing identities fall through to - * the relevant fallback strings from `formatUsernameLine`. + * Both the SS58 and the 0x H160 are printed in full so the user can copy + * them directly. The username lookup is async (queries People parachain) + * and has a 10s timeout inside `lookupUsername`; the product account is + * synchronous (pure sr25519 soft derivation). A `(looking up...)` + * placeholder renders while the lookup is in flight; failures and missing + * identities fall through to the relevant fallback strings from + * `formatUsernameLine`. */ export function IdentityLines({ address }: { address: string }) { const [username, setUsername] = useState({ kind: "loading" }); diff --git a/src/commands/init/identityLine.test.ts b/src/commands/init/identityLine.test.ts index 384122f..2a077e2 100644 --- a/src/commands/init/identityLine.test.ts +++ b/src/commands/init/identityLine.test.ts @@ -27,7 +27,11 @@ describe("productAccountAddresses", () => { it("derives a non-empty SS58 + a 42-char H160 from a root SS58", () => { const { ss58, h160 } = productAccountAddresses(ZERO_ROOT_SS58); expect(typeof ss58).toBe("string"); - expect(ss58.length).toBeGreaterThan(40); + // Substrate SS58 addresses for a 32-byte pubkey are 47–48 chars on + // the default ss58Format=42 prefix. Anything shorter would mean + // we'd accidentally re-introduced truncation. + expect(ss58.length).toBeGreaterThanOrEqual(47); + expect(ss58).toMatch(/^[1-9A-HJ-NP-Za-km-z]+$/); expect(h160).toMatch(/^0x[0-9a-fA-F]{40}$/); }); @@ -40,8 +44,12 @@ describe("productAccountAddresses", () => { }); describe("productAccountDisplay", () => { - it("renders 'ss58 (h160)' with both addresses truncated", () => { + it("renders 'ss58 (h160)' with the full SS58 + full 0x-prefixed H160", () => { const display = productAccountDisplay(ZERO_ROOT_SS58); - expect(display).toMatch(/^.+\.\.\..+ \(0x.+\.\.\..+\)$/); + const match = display.match(/^([1-9A-HJ-NP-Za-km-z]+) \((0x[0-9a-fA-F]{40})\)$/); + expect(match).not.toBeNull(); + // No ellipses anywhere — the whole point of the change is that we + // print the full address so the user can copy it directly. + expect(display).not.toContain("..."); }); }); diff --git a/src/commands/init/identityLine.ts b/src/commands/init/identityLine.ts index 5265686..0bd8672 100644 --- a/src/commands/init/identityLine.ts +++ b/src/commands/init/identityLine.ts @@ -20,7 +20,7 @@ * `completion.ts` next to `InitScreen.tsx` for the same pattern). */ -import { deriveH160, ss58Decode, ss58Encode, truncateAddress } from "@parity/product-sdk-address"; +import { deriveH160, ss58Decode, ss58Encode } from "@parity/product-sdk-address"; import { deriveProductAccountPublicKey } from "@parity/product-sdk-keys"; import { PLAYGROUND_PRODUCT_ID } from "../../config.js"; @@ -40,5 +40,5 @@ export function productAccountAddresses(rootAccountSs58: string): ProductAccount export function productAccountDisplay(rootAccountSs58: string): string { const { ss58, h160 } = productAccountAddresses(rootAccountSs58); - return `${truncateAddress(ss58, 6, 4)} (${truncateAddress(h160, 6, 4)})`; + return `${ss58} (${h160})`; } diff --git a/src/utils/username.test.ts b/src/utils/username.test.ts index 4fe5e98..156ca7d 100644 --- a/src/utils/username.test.ts +++ b/src/utils/username.test.ts @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { describe, expect, it } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { formatUsernameLine, type UsernameLookup } from "./username.js"; describe("formatUsernameLine", () => { @@ -53,3 +53,98 @@ describe("formatUsernameLine", () => { expect(formatUsernameLine(lookup)).toBe("(looking up...)"); }); }); + +// Mocks must be set up at module-load time so the polkadot-api imports inside +// `username.ts` resolve to our stubs. The pattern mirrors `connection.test.ts`. +const mockGetValues = vi.fn(); +const mockCreateClient = vi.fn(); +const mockGetWsProvider = vi.fn(); +const mockDestroy = vi.fn(); + +vi.mock("polkadot-api", () => ({ + createClient: (provider: unknown) => mockCreateClient(provider), +})); + +vi.mock("polkadot-api/ws", () => ({ + getWsProvider: (endpoints: unknown) => mockGetWsProvider(endpoints), +})); + +describe("lookupUsername", () => { + beforeEach(() => { + vi.resetModules(); + mockGetValues.mockReset(); + mockCreateClient.mockReset(); + mockGetWsProvider.mockReset(); + mockDestroy.mockReset(); + + mockGetWsProvider.mockImplementation(() => ({})); + mockCreateClient.mockImplementation(() => ({ + destroy: mockDestroy, + getUnsafeApi: () => ({ + query: { + Resources: { + Consumers: { + getValues: mockGetValues, + }, + }, + }, + }), + })); + }); + + // Regression guard: under scale-ts's `fromHex`-based string decoder, + // routing the SS58 through `AccountId().dec(...)` silently corrupts it + // (most SS58 chars aren't in `HEX_MAP`) and the storage call surfaces as + // `(lookup failed)`. The whole bug class disappears as long as we pass + // the SS58 string through unchanged — this test fails if anyone + // reintroduces a codec round-trip. + it("passes the SS58 string directly to getValues, with no codec round-trip", async () => { + const ss58 = "5GGpUaN7XNaUp3nEVDPBSR4SQLxFxQsiPHbFwf69Apr3HgDZ"; + mockGetValues.mockResolvedValue([null]); + + const { lookupUsername } = await import("./username.js"); + const result = await lookupUsername(ss58); + + expect(mockGetValues).toHaveBeenCalledTimes(1); + expect(mockGetValues).toHaveBeenCalledWith([[ss58]]); + expect(result).toEqual({ kind: "none" }); + }); + + it("returns 'found' with decoded usernames when the chain has a record", async () => { + const fullUsername = new TextEncoder().encode("alice.dot"); + const liteUsername = new TextEncoder().encode("alice"); + mockGetValues.mockResolvedValue([ + { full_username: fullUsername, lite_username: liteUsername, credibility: null }, + ]); + + const { lookupUsername } = await import("./username.js"); + const result = await lookupUsername("5GGpUaN7XNaUp3nEVDPBSR4SQLxFxQsiPHbFwf69Apr3HgDZ"); + + expect(result).toEqual({ + kind: "found", + fullUsername: "alice.dot", + liteUsername: "alice", + }); + }); + + it("returns 'error' if the Resources.Consumers storage entry is missing from chain metadata", async () => { + mockCreateClient.mockImplementation(() => ({ + destroy: mockDestroy, + getUnsafeApi: () => ({ + query: { Resources: undefined }, + }), + })); + + const { lookupUsername } = await import("./username.js"); + const result = await lookupUsername("5GGpUaN7XNaUp3nEVDPBSR4SQLxFxQsiPHbFwf69Apr3HgDZ"); + + expect(result.kind).toBe("error"); + }); + + it("destroys the per-call client to release the WebSocket", async () => { + mockGetValues.mockResolvedValue([null]); + const { lookupUsername } = await import("./username.js"); + await lookupUsername("5GGpUaN7XNaUp3nEVDPBSR4SQLxFxQsiPHbFwf69Apr3HgDZ"); + expect(mockDestroy).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/utils/username.ts b/src/utils/username.ts index c5bac1e..dfe7489 100644 --- a/src/utils/username.ts +++ b/src/utils/username.ts @@ -26,16 +26,32 @@ * piece we do need: the storage query + the byte mapping. Same precedent as * `src/utils/allowances/host.ts` (which mirrors host-papp's RFC-0010 call). * - * The upstream source we're mirroring lives in - * `@novasamatech/host-papp@0.7.9-4/dist/identity/rpcAdapter.js`; keep this in - * sync if that ever changes pallet/storage names. + * NOTE on the storage key: `unsafeApi.query.Resources.Consumers.getValues` + * expects the key in JS form — for `AccountId32`, that's an SS58 string. The + * upstream `createIdentityRpcAdapter` runs the input through + * `AccountId().dec(x)` because *its* caller passes a 0x-prefixed pubkey hex + * (see dotli `packages/auth/src/auth.ts`, which calls `getIdentity(\`0x${pk}\`)`), + * so the `.dec` round-trips hex → SS58 before handing it to PAPI. We already + * receive the SS58 string from the QR-login flow, so the `.dec` step would + * silently corrupt it: under the hood `.dec` runs the string through + * scale-ts's `fromHex`, which reads each character via `HEX_MAP[ch]` — most + * SS58 chars (`G`, `H`, `J`, `K`, `P`, `U`, `p`, `r`, …) aren't in the map + * so they coerce to 0 (`undefined << 4 | undefined` → `0`). The resulting + * mostly-zero buffer is then re-encoded by `fromBufferToBase58` into a + * malformed SS58 (wrong length, wrong checksum). That bogus key is what + * gets handed to PAPI's storage encoder, where `getSs58AddressInfo` rejects + * it and the lookup surfaces as `(lookup failed)`. Pass the SS58 directly. */ -import { AccountId, createClient } from "polkadot-api"; +import { createClient } from "polkadot-api"; import { getWsProvider } from "polkadot-api/ws"; import { getChainConfig } from "../config.js"; -const LOOKUP_TIMEOUT_MS = 5_000; +// Cold-start WS connects to paseo-people-next-system-rpc on a slow conference +// network can take a few seconds before metadata + the first query are ready. +// The success path is sub-second on a fast network; the 10s budget only kicks +// in when the chain is genuinely unreachable. +const LOOKUP_TIMEOUT_MS = 10_000; export type UsernameLookup = | { kind: "loading" } @@ -79,8 +95,6 @@ export async function lookupUsername(rootAccountSs58: string): Promise>, + query.getValues([[rootAccountSs58]]) as Promise< + Array + >, new Promise<"timeout">((resolve) => setTimeout(() => resolve("timeout"), LOOKUP_TIMEOUT_MS), ),