diff --git a/.cdm/cdm.d.ts b/.cdm/cdm.d.ts new file mode 100644 index 00000000..a1696028 --- /dev/null +++ b/.cdm/cdm.d.ts @@ -0,0 +1,85 @@ +// Auto-generated by cdm install -- do not edit +import type { HexString, SizedHex } from "polkadot-api"; + +declare module "@parity/cdm-codegen" { + interface CdmContracts { + "@w3s/playground-registry": { + methods: { + getAppCount: { args: []; response: number }; + getDomainAt: { args: [index: number]; response: string }; + getMetadataUri: { args: [domain: string]; response: string }; + getOwner: { args: [domain: string]; response: HexString }; + getVisibility: { args: [domain: string]; response: number }; + getOwnerAppCount: { args: [owner: HexString]; response: number }; + getOwnerDomainAt: { args: [owner: HexString, index: number]; response: string }; + getSudo: { args: []; response: HexString }; + isAdmin: { args: [addr: HexString]; response: boolean }; + isBlacklisted: { args: [account: HexString]; response: boolean }; + getPoints: { args: [account: HexString]; response: bigint }; + getApps: { args: [start: number, count: number]; response: { total: number; scanned: number; entries: { index: number; domain: string; metadata_uri: string; owner: HexString; visibility: number; publisher: HexString }[] } }; + getStarCount: { args: [domain: string]; response: number }; + getModCount: { args: [domain: string]; response: number }; + getLineageCount: { args: []; response: number }; + getLineage: { args: [start: number, count: number]; response: { child: string; source: string }[] }; + hasStarred: { args: [voter: HexString, domain: string]; response: boolean }; + getAppData: { args: [domains: string[], voter: HexString]; response: { domain: string; metadata_uri: string; owner: HexString; visibility: number; publisher: HexString; star_count: number; mod_count: number; has_starred: boolean }[] }; + getPointBreakdown: { args: [account: HexString]; response: { mod_points: bigint; star_points: bigint; total: bigint } }; + isPinned: { args: [domain: string]; response: boolean }; + getPinnedApps: { args: []; response: { index: number; domain: string; metadata_uri: string; owner: HexString; visibility: number; publisher: HexString }[] }; + getBuilderCount: { args: []; response: number }; + getBuilders: { args: [start: number, count: number]; response: { total: number; scanned: number; entries: { account: HexString; score: bigint }[] } }; + pin: { args: [domain: string]; response: undefined }; + unpin: { args: [domain: string]; response: undefined }; + star: { args: [domain: string]; response: undefined }; + publish: { args: [domain: string, metadata_uri: string, visibility: number, modded_from: string]; response: undefined }; + setVisibility: { args: [domain: string, visibility: number]; response: undefined }; + unpublish: { args: [domain: string]; response: undefined }; + addAdmin: { args: [admin: HexString]; response: undefined }; + removeAdmin: { args: [admin: HexString]; response: undefined }; + adminSetDeployAwardCount: { args: [account: HexString, count: number]; response: undefined }; + adminSetAppSocialCounts: { args: [domain: string, star_count: number, mod_count: number]; response: undefined }; + setBlacklisted: { args: [accounts: HexString[], value: boolean]; response: undefined }; + setFrozen: { args: [value: boolean]; response: undefined }; + isFrozen: { args: []; response: boolean }; + setVerifier: { args: [verifier: HexString]; response: undefined }; + getVerifier: { args: []; response: HexString }; + importApp: { args: [domain: string, owner: HexString, publisher: HexString, visibility: number, metadata_uri: string]; response: undefined }; + importApps: { args: [apps: { domain: string; owner: HexString; publisher: HexString; visibility: number; metadata_uri: string }[]]; response: undefined }; + importPinned: { args: [domain: string]; response: undefined }; + importLineage: { args: [entries: { child: string; source: string }[]]; response: undefined }; + importPoints: { args: [entries: { account: HexString; total: bigint }[]]; response: undefined }; + importSocialCounts: { args: [entries: { domain: string; star_count: number; mod_count: number }[]]; response: undefined }; + }; + }; + "@w3s/playground-identity": { + methods: { + setIdentity: { args: [root_pubkey: SizedHex<32>, signature: number[]]; response: undefined }; + clearIdentity: { args: []; response: undefined }; + isVerified: { args: [account: HexString]; response: boolean }; + rootOf: { args: [account: HexString]; response: SizedHex<32> }; + getRootAccount: { args: [account: HexString]; response: SizedHex<32> }; + getRootAccounts: { args: [accounts: HexString[]]; response: SizedHex<32>[] }; + adminSetIdentity: { args: [account: HexString, root_pubkey: SizedHex<32>]; response: undefined }; + adminClearIdentity: { args: [account: HexString]; response: undefined }; + addAdmin: { args: [admin: HexString]; response: undefined }; + removeAdmin: { args: [admin: HexString]; response: undefined }; + setFrozen: { args: [value: boolean]; response: undefined }; + importIdentities: { args: [accounts: HexString[], roots: SizedHex<32>[]]; response: undefined }; + }; + }; + "@w3s/playground-open-verifier": { + methods: { + isVerified: { args: [_account: HexString]; response: boolean }; + rootOf: { args: [account: HexString]; response: SizedHex<32> }; + getRootAccount: { args: [account: HexString]; response: SizedHex<32> }; + getRootAccounts: { args: [accounts: HexString[]]; response: SizedHex<32>[] }; + faucet: { args: []; response: undefined }; + canClaim: { args: [account: HexString]; response: boolean }; + lastClaimAt: { args: [account: HexString]; response: number }; + setFrozen: { args: [value: boolean]; response: undefined }; + getSudo: { args: []; response: HexString }; + isFrozen: { args: []; response: boolean }; + }; + }; + } +} diff --git a/.cdm/contracts.d.ts b/.cdm/contracts.d.ts new file mode 100644 index 00000000..492ea946 --- /dev/null +++ b/.cdm/contracts.d.ts @@ -0,0 +1,85 @@ +// Auto-generated by cdm install -- do not edit +import type { HexString, SizedHex } from "polkadot-api"; + +declare module "@parity/product-sdk-contracts" { + interface Contracts { + "@w3s/playground-registry": { + methods: { + getAppCount: { args: []; response: number }; + getDomainAt: { args: [index: number]; response: string }; + getMetadataUri: { args: [domain: string]; response: string }; + getOwner: { args: [domain: string]; response: HexString }; + getVisibility: { args: [domain: string]; response: number }; + getOwnerAppCount: { args: [owner: HexString]; response: number }; + getOwnerDomainAt: { args: [owner: HexString, index: number]; response: string }; + getSudo: { args: []; response: HexString }; + isAdmin: { args: [addr: HexString]; response: boolean }; + isBlacklisted: { args: [account: HexString]; response: boolean }; + getPoints: { args: [account: HexString]; response: bigint }; + getApps: { args: [start: number, count: number]; response: { total: number; scanned: number; entries: { index: number; domain: string; metadata_uri: string; owner: HexString; visibility: number; publisher: HexString }[] } }; + getStarCount: { args: [domain: string]; response: number }; + getModCount: { args: [domain: string]; response: number }; + getLineageCount: { args: []; response: number }; + getLineage: { args: [start: number, count: number]; response: { child: string; source: string }[] }; + hasStarred: { args: [voter: HexString, domain: string]; response: boolean }; + getAppData: { args: [domains: string[], voter: HexString]; response: { domain: string; metadata_uri: string; owner: HexString; visibility: number; publisher: HexString; star_count: number; mod_count: number; has_starred: boolean }[] }; + getPointBreakdown: { args: [account: HexString]; response: { mod_points: bigint; star_points: bigint; total: bigint } }; + isPinned: { args: [domain: string]; response: boolean }; + getPinnedApps: { args: []; response: { index: number; domain: string; metadata_uri: string; owner: HexString; visibility: number; publisher: HexString }[] }; + getBuilderCount: { args: []; response: number }; + getBuilders: { args: [start: number, count: number]; response: { total: number; scanned: number; entries: { account: HexString; score: bigint }[] } }; + pin: { args: [domain: string]; response: undefined }; + unpin: { args: [domain: string]; response: undefined }; + star: { args: [domain: string]; response: undefined }; + publish: { args: [domain: string, metadata_uri: string, visibility: number, modded_from: string]; response: undefined }; + setVisibility: { args: [domain: string, visibility: number]; response: undefined }; + unpublish: { args: [domain: string]; response: undefined }; + addAdmin: { args: [admin: HexString]; response: undefined }; + removeAdmin: { args: [admin: HexString]; response: undefined }; + adminSetDeployAwardCount: { args: [account: HexString, count: number]; response: undefined }; + adminSetAppSocialCounts: { args: [domain: string, star_count: number, mod_count: number]; response: undefined }; + setBlacklisted: { args: [accounts: HexString[], value: boolean]; response: undefined }; + setFrozen: { args: [value: boolean]; response: undefined }; + isFrozen: { args: []; response: boolean }; + setVerifier: { args: [verifier: HexString]; response: undefined }; + getVerifier: { args: []; response: HexString }; + importApp: { args: [domain: string, owner: HexString, publisher: HexString, visibility: number, metadata_uri: string]; response: undefined }; + importApps: { args: [apps: { domain: string; owner: HexString; publisher: HexString; visibility: number; metadata_uri: string }[]]; response: undefined }; + importPinned: { args: [domain: string]; response: undefined }; + importLineage: { args: [entries: { child: string; source: string }[]]; response: undefined }; + importPoints: { args: [entries: { account: HexString; total: bigint }[]]; response: undefined }; + importSocialCounts: { args: [entries: { domain: string; star_count: number; mod_count: number }[]]; response: undefined }; + }; + }; + "@w3s/playground-identity": { + methods: { + setIdentity: { args: [root_pubkey: SizedHex<32>, signature: number[]]; response: undefined }; + clearIdentity: { args: []; response: undefined }; + isVerified: { args: [account: HexString]; response: boolean }; + rootOf: { args: [account: HexString]; response: SizedHex<32> }; + getRootAccount: { args: [account: HexString]; response: SizedHex<32> }; + getRootAccounts: { args: [accounts: HexString[]]; response: SizedHex<32>[] }; + adminSetIdentity: { args: [account: HexString, root_pubkey: SizedHex<32>]; response: undefined }; + adminClearIdentity: { args: [account: HexString]; response: undefined }; + addAdmin: { args: [admin: HexString]; response: undefined }; + removeAdmin: { args: [admin: HexString]; response: undefined }; + setFrozen: { args: [value: boolean]; response: undefined }; + importIdentities: { args: [accounts: HexString[], roots: SizedHex<32>[]]; response: undefined }; + }; + }; + "@w3s/playground-open-verifier": { + methods: { + isVerified: { args: [_account: HexString]; response: boolean }; + rootOf: { args: [account: HexString]; response: SizedHex<32> }; + getRootAccount: { args: [account: HexString]; response: SizedHex<32> }; + getRootAccounts: { args: [accounts: HexString[]]; response: SizedHex<32>[] }; + faucet: { args: []; response: undefined }; + canClaim: { args: [account: HexString]; response: boolean }; + lastClaimAt: { args: [account: HexString]; response: number }; + setFrozen: { args: [value: boolean]; response: undefined }; + getSudo: { args: []; response: HexString }; + isFrozen: { args: []; response: boolean }; + }; + }; + } +} diff --git a/.changeset/bulletin-deploy-0200.md b/.changeset/bulletin-deploy-0200.md new file mode 100644 index 00000000..81359303 --- /dev/null +++ b/.changeset/bulletin-deploy-0200.md @@ -0,0 +1,22 @@ +--- +"playground-cli": patch +--- + +Update `bulletin-deploy` to 0.20.0. + +Fixes two crashes that ended a deploy after the content was already uploaded. +One of them is a crash we hit in practice: `JSON.parse` on incoming RPC data was +unguarded and threw inside the socket handler, where nothing upstream could +catch it, killing the process mid-deploy. The other is `hasIPFS()` passing when +`ipfs` is installed but was never initialised. + +Also brings, from 0.19.x: the storage-deposit floor for registering a name drops +from a hardcoded 200 PAS to 5 PAS (measured cost is ~0.15), registers no longer +revert when the chain moves between dry-run and inclusion, duplicate files in a +bundle upload once instead of twice, an interrupted register no longer strands +the commitment fee, Bulletin re-authorizes on an exhausted-but-unexpired quota, +and deploying refuses to silently fall back to the public dev key when a stored +session can't be read. + +Additive for us: `deploy()`'s signature is unchanged, every option we pass still +exists, and the log banners the progress bar parses are intact. diff --git a/.changeset/fix-dotns-generation-drift.md b/.changeset/fix-dotns-generation-drift.md new file mode 100644 index 00000000..f16c193d --- /dev/null +++ b/.changeset/fix-dotns-generation-drift.md @@ -0,0 +1,23 @@ +--- +"playground-cli": patch +--- + +Fix deploys failing with `Contract execution would revert during startingPrice on POP_RULES`. + +The DotNS contracts ship in ABI *generations* that are redeployed behind the same +contract addresses, and roll out per environment rather than per release. Generation +`v0.5.8-rc1` removed `PopRules.startingPrice()`, so the pinned `bulletin-deploy@0.15.0` +— which only knows the older generation and calls that function unconditionally — hit a +revert on paseo-next-v2 for every deploy, dev mode included. Because the addresses never +changed, this was invisible to any version diff of `environments.json`. + +Bumps `bulletin-deploy` to `0.18.4`, which probes the live generation at connect time and +supports all three. + +Also fixes the domain-availability preflight, which constructed `DotNS` and connected with +only an RPC URL. Without a contract map it fell back to built-in defaults whose `POP_RULES` +has no code on this network, so 0.16+'s generation probe failed with "No contract deployed +at this address". It now resolves the env's contracts from bulletin-deploy's own +`environments.json` and passes them (plus `environmentId`) to `connect()`, matching what the +deploy path already did. The divergence guard in `config.test.ts` now asserts that contract +map exists upstream, so a catalog change fails in CI rather than mid-deploy. diff --git a/.changeset/gate-asks-wired-verifier.md b/.changeset/gate-asks-wired-verifier.md new file mode 100644 index 00000000..2308fd55 --- /dev/null +++ b/.changeset/gate-asks-wired-verifier.md @@ -0,0 +1,20 @@ +--- +"playground-cli": patch +--- + +Ask the personhood verifier the registry actually uses before blocking a +command. + +`playground deploy`, `deploy-all`, `mod` and `decentralize` refused with +"Join the competition first" for users whose publish the chain would have +accepted. The gate read `getRootAccount` from the identity spine, but the +registry does not consult the spine — it consults whatever `set_verifier` +names, which on the current deployment is an open verifier that accepts every +account. The CLI was enforcing a stricter rule than the chain, against a +different contract. + +The gate now resolves `registry.getVerifier()` and calls `isVerified` on that +address, so it is correct both today and on a competition deployment that +wires the spine, with no flag to keep in sync. It asks the verifier a +question rather than comparing its address — an address comparison would be +correct only until the next verifier implementation. diff --git a/.changeset/product-id-follows-env-tld.md b/.changeset/product-id-follows-env-tld.md new file mode 100644 index 00000000..4cebd56c --- /dev/null +++ b/.changeset/product-id-follows-env-tld.md @@ -0,0 +1,21 @@ +--- +"playground-cli": minor +--- + +Derive the product account from an environment-scoped product id +(`playground.paseo` on paseo-next-v2) instead of a fixed `playground.dot`. + +This is an identity migration, not a cosmetic change: the product id is an +input to the account derivation, so **your product account address changes**. +Apps published from the CLI before this release are owned by the old address +and will no longer show as yours; anything keyed to the old product id (cached +subtrees, the SDK allowance cache under `~/.polkadot-apps/`) is simply not +found under the new one and is re-granted at your next `playground login`. + +The change is required for the CLI to work at all against a phone on a +`paseo` environment. The mobile host validates the product id against its own +TLD and drops a mismatch silently, so a `.dot` id produced no prompt, no error +and no reply — only a ~180 second timeout. It also keeps the CLI and the +playground web app on one identity: the app already ships the +environment-suffixed form, and two different ids for the same person would +split app ownership and XP across two accounts. diff --git a/.changeset/rfc0022-product-accounts.md b/.changeset/rfc0022-product-accounts.md new file mode 100644 index 00000000..24953cf6 --- /dev/null +++ b/.changeset/rfc0022-product-accounts.md @@ -0,0 +1,32 @@ +--- +"playground-cli": minor +--- + +Fix QR pairing, and sign as the RFC-0022 product account. + +`playground login` has been unable to pair with current phones since +mid-August: `@novasamatech/host-papp` 0.9.0 moved the pairing envelope from +P-256/AES-GCM to X25519/ChaCha20-Poly1305, shrinking the device key from 65 to +32 bytes. The handshake codec is fixed-width, so there was no graceful +degrade — iOS reported "Something went wrong" and Android "invalid QR code". +Moving to `@parity/product-sdk-terminal@0.10.0` (host-papp 0.10.0) puts the CLI +back on the wire both shipping apps speak. + +The same release changes how the product account is derived. RFC-0022 places it +at `//product//{productId}/{index}`, where the two `//product//{productId}` +junctions are HARD — and a public key cannot cross a hard junction, so the +account is now derived from a subtree key the wallet provides rather than from +the session's root key locally. + +**Your product account address changes, and you must pair again.** Run +`playground logout` and then `playground login`. Apps published from the old +address stay owned by it; allowances and PGAS are re-granted at the next login. +The new derivation is what current phones and the playground web app already +use, so this brings the CLI back into agreement with them rather than away. + +Deploy also stops presenting a retry as progress. The phone-approval counter +was counting signature *requests*, so a request that timed out and was re-sent +appeared as the next step — and contradicted the "Phone approvals expected" +plan, which counts operations. A deploy could print "step 2: Link content" +while the plan said step 2 was the registry publish. Repeats are now labelled +as retries, with the remedy worth checking when no prompt appeared at all. diff --git a/.changeset/v2-registry-contracts.md b/.changeset/v2-registry-contracts.md new file mode 100644 index 00000000..bb2cf482 --- /dev/null +++ b/.changeset/v2-registry-contracts.md @@ -0,0 +1,21 @@ +--- +"playground-cli": minor +--- + +Move to the v2 playground contracts (registry + identity + verifier). + +`registry.publish` now takes four arguments (`domain`, `metadata_uri`, +`visibility`, `modded_from`) and has a new selector, so this release is +required to publish at all — the previous seven-argument form reverts against +the deployed contract. The dropped `owner` parameter changes who owns an app: +the registry now always records the caller, so a dev-mode deploy is owned by +the dev signer and will not appear in your MyApps. `playground deploy` says so +explicitly in its summary instead of implying otherwise. + +Also fixes `playground mod` and `playground init`, which failed with +`App "" not found in registry` for every app, including ones that +demonstrably existed. The v2 registry returns `getMetadataUri` as a plain +string (empty when absent) rather than an `Option`, and the old +`value.isSome` narrowing read `undefined` every time. Decoding now lives in +one place and treats an unexpected payload as an error rather than silently +reporting "not found". diff --git a/.gitignore b/.gitignore index eac1ca67..2b37f56f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,15 @@ node_modules/ dist/ -.cdm/ +# Generated by `cdm install`. The TYPE augmentations ARE committed (below) so +# CI typechecks contract call sites — without them the registry handle is a bare +# `Contract` and every call silently goes unchecked. The resolved +# artifacts under .cdm/contracts/ are NOT committed: they snapshot per-version +# contract addresses we never read (addresses resolve live from the CDM +# meta-registry), so they would be a second, stale source of truth. +.cdm/* +!.cdm/cdm.d.ts +!.cdm/contracts.d.ts docs-internal/ docs/superpowers/ .worktrees/ diff --git a/CLAUDE.md b/CLAUDE.md index 2a531257..f8cc53fd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -13,7 +13,11 @@ pnpm typecheck pnpm test ``` -`pnpm build` compiles with bun, which STRIPS types without checking them — it is NOT a type signal. The tsc baseline was burned to zero in June 2026 and `pnpm typecheck` (`tsc --noEmit`) now runs as a CI job, so any new type error fails the build — keep it at zero. The cdm-builder version skew is bridged through THREE cast seams: `asCloudStorageApi` in `src/utils/allowances/bulletin.ts` (the `TypedApi` vs `BulletinTypedApi` codegen skew — two codegens of the same chain), plus `asCdmAssetHubApi`/`asCdmAssetHubDescriptor` and `asCdmRegistryContract` in `src/commands/contract.ts`. The asset-hub seams bridge the paseo-asset-hub `DotnsGateway`/descriptor shape drift between cdm-builder's pinned `@parity/product-sdk-descriptors` and our root's (the contract pipeline never touches DotnsGateway, so the cast is runtime-safe). `asCdmRegistryContract` was added when our root `@parity/product-sdk-contracts` reached `0.9.x`: `Contract.tx()` now resolves a `Result`, nominally unassignable to cdm-builder's older `RegistryContract` param — but the install-time registry is query-only (no signer wired in), so `.tx()` is never invoked through it and the cast is runtime-safe. At `cdm-builder@3.2.0` the pinned product-sdk moved up to `contracts@0.9.0` / `descriptors@0.7.0` while our root is `contracts@^0.9.2` / `descriptors@^0.8.0` — so the `.tx()` Result shape realigns (making `asCdmRegistryContract` likely redundant) while a descriptors minor gap (0.7 vs 0.8) remains. Before deleting any seam, remove it and run `pnpm typecheck`. 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. +**🔴 `pnpm