From d859c74b3bf26d61c1cb4699e3ef13c16c1c3b42 Mon Sep 17 00:00:00 2001 From: Reinhard Hatko Date: Mon, 21 Sep 2026 22:27:12 +0800 Subject: [PATCH 1/8] feat(contracts): migrate to the v2 playground registry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit registry #525 changed `publish` to four arguments (domain, metadata_uri, visibility, modded_from) and changed the selector, so the previous seven-argument call reverts against the deployed contract. The dropped `owner` parameter is the behavioural change worth knowing: the registry now always records `env::caller()`, so dev mode can no longer publish under the signed-in user's identity. `claimedOwnerH160` was renamed `userSessionH160` because it no longer claims anything — it only drives the deploy summary, which now says plainly that a dev-mode app is owned by the dev signer and won't show in MyApps. `is_moddable` and `is_dev_signer` are gone too; moddability lives in the metadata JSON's `repository` field. Also fixes `playground mod` and `playground init`, which failed with `App "" not found in registry` for every app, including ones that exist. v2 returns `getMetadataUri` as a plain string (empty when absent) rather than an Option, and the `value.isSome` narrowing at both call sites read `undefined` every time. Decoding now lives in one helper that treats an unexpected payload as an error instead of silently reporting "not found" — the shape drift was invisible to tsc because both call sites were untyped. --- .changeset/v2-registry-contracts.md | 21 + CLAUDE.md | 7 +- cdm.json | 1556 +++++++++++++++----------- src/commands/deploy/DeployScreen.tsx | 2 +- src/commands/deploy/index.ts | 2 +- src/commands/deploy/summary.test.ts | 21 +- src/commands/deploy/summary.ts | 18 +- src/commands/mod/SetupScreen.tsx | 9 +- src/commands/mod/index.ts | 14 +- src/utils/decentralize/run.test.ts | 6 +- src/utils/decentralize/run.ts | 7 +- src/utils/deploy/playground.test.ts | 111 +- src/utils/deploy/playground.ts | 59 +- src/utils/deploy/run.test.ts | 36 +- src/utils/deploy/run.ts | 3 - src/utils/deploy/signerMode.test.ts | 6 +- src/utils/deploy/signerMode.ts | 45 +- src/utils/mod/metadataUri.test.ts | 70 ++ src/utils/mod/metadataUri.ts | 58 + src/utils/signer.ts | 3 +- 20 files changed, 1141 insertions(+), 913 deletions(-) create mode 100644 .changeset/v2-registry-contracts.md create mode 100644 src/utils/mod/metadataUri.test.ts create mode 100644 src/utils/mod/metadataUri.ts 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/CLAUDE.md b/CLAUDE.md index 4ef4e9ee..a1cc8c5b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -43,11 +43,12 @@ These aren't self-evident from reading the code and have bitten us before. Treat ### Deploy / Bulletin - **Bulletin storage chunks must NEVER sign with the phone session signer.** Chunk txs carry up to 2 MiB of callData; the phone path (`session.createTransaction`) forwards the full callData over the statement store, whose host-side request cap is 4 KiB (`DEFAULT_MAX_REQUEST_SIZE = 4096`, unchanged through host-papp 0.8.x; Android's own statement cap grew to 256 KiB in May 2026 but 2 MiB chunks exceed both), so every chunk dies client-side with "Mobile transaction signing rejected: message too big" and the phone never even shows a prompt. Since polkadot-app-deploy 0.8.x, passing `signer` routes STORAGE through it too (not just DotNS), so phone mode must also pass `storageSigner`/`storageSignerAddress` (the local BulletInAllowance slot key, which takes precedence for storage routing only). `src/utils/deploy/signerMode.ts::resolveStorageSignerOptions` is the single place that resolves it; both `runDeploy` and `runDecentralize` thread it into `runStorageDeploy`. polkadot-app-deploy (0.8.3 through 0.13.1) can auto-resolve the same slot key from the shared `dot-cli` allowance cache, but silently falls back to phone-signing the chunks when it misses, so don't rely on it. -- **Deploy delegates to `polkadot-app-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 takes an `Option
owner` parameter — when None, it falls back to `env::caller()`; when Some, that H160 is recorded as the app owner regardless of who signed. Phone mode passes None (caller IS the user). Dev mode with an active session passes the session's `productH160` so Alice can sign the tx while the user still appears in MyApps. The `publisher` field on `AppInfo` always stores `env::caller()`, so `is_authorized_to_republish` lets the original signer iterate without rewriting ownership. See `src/utils/deploy/playground.ts` and `src/utils/deploy/signerMode.ts::resolveSignerSetup`. -- **Do NOT call `polkadot-app-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 signed with the user's product account in phone mode, and with a dev signer in dev mode (claimed-owner H160 carries the user identity, per the bullet above). See `src/utils/deploy/playground.ts::publishToPlayground`. +- **Deploy delegates to `polkadot-app-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()`. **On the v2 registry that is `publish(domain, metadata_uri, visibility, modded_from)` — four args, and the SELECTOR CHANGED, so an older CLI reverts against the deployed contract.** The `owner` parameter is GONE: the registry always records `env::caller()` as owner, and there is no way to publish on someone else's behalf. That kills the old "dev mode claims the user's identity" trick — a dev-mode deploy is owned by the dev signer and will NOT appear in the user's MyApps, which `src/commands/deploy/summary.ts` now states outright instead of implying otherwise. `is_moddable` and `is_dev_signer` are gone too: moddability is expressed by `metadata.repository` in the off-chain metadata JSON (see the `--moddable` invariant below), not by a contract flag. See `src/utils/deploy/playground.ts` and `src/utils/deploy/signerMode.ts::resolveSignerSetup`. +- **The v2 registry returns `getMetadataUri` as a PLAIN STRING, not `Option` — an absent app is the EMPTY STRING.** Decoding lives in exactly one place (`src/utils/mod/metadataUri.ts::getAppMetadataUri`); keep it there. The pre-v2 contract did return an Option, and the call sites narrowed with `value.isSome ? value.value : null` — against a plain string that reads `undefined` every time, so `playground mod` and `playground init` reported EVERY app as "not found in registry", including ones that demonstrably existed. Both call sites were typed `any`/unnarrowed, so neither `tsc` nor the tests could see it (same class as the cast seams above — and the reason CI must generate `.cdm/` and typecheck contract call sites). The decoder throws on an unexpected payload rather than degrading to "not found": a silent wrong-shape read is exactly this bug, and a missing app and a changed ABI need different responses. `getApps` is `{total, scanned, entries}` (`AppBrowser.tsx` already handles it) — verify a shape against the chain before trusting an older code path. +- **Do NOT call `polkadot-app-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 signed with the user's product account in phone mode, and with a dev signer in dev mode — and since v2 the signer IS the recorded owner, so dev mode cannot publish under the user's identity (per the bullet above). See `src/utils/deploy/playground.ts::publishToPlayground`. - **Dev mode must pass EXPLICIT auth options to `polkadot-app-deploy.deploy()` — never `{}`.** Since 0.8.x (the "#411 login UX"), `deploy()` called with no `mnemonic`, no `signer`, and no `suri` probes for a persisted SSO session file and, when found, loads the SSO stack and phone-signs DotNS with the user's session. ⚠️ **CORRECTED Sept 2026: bulletin-deploy no longer shares our `dot-cli` storage namespace.** Its `DOT_DAPP_ID` is `"polkadot-app-deploy"` — re-verify against whatever is pinned with `grep -r DOT_DAPP_ID node_modules/bulletin-deploy/dist/`; it holds at the pinned `0.18.4` (`dist/auth-config.d.ts`) and in the 0.18.2 source (`src/auth-config.ts:22`), with no `dot-cli` literal anywhere in the dist. So its session probe reads `~/.polkadot-apps/polkadot-app-deploy_SsoSessions*.json`, NOT the `dot-cli_*` files `playground login` writes, and it can no longer pick up a logged-in playground user's session. The earlier claim that it "reuses `DOT_DAPP_ID = "dot-cli"`" was true of the older `@parity/polkadot-app-deploy` line and is now stale — do not re-derive defenses from it. Keep passing explicit auth options anyway: the storageSigner half of this invariant (below) is unaffected by the namespace split, and a future rename could flip it back — turning a "0 taps" dev deploy into 3-4 phone approvals for every logged-in user. Independently, an absent `storageSigner` makes it auto-read the user's cached BulletInAllowance slot key and burn their small phone-granted quota on chunk uploads, in every mode including `--suri`. `resolveSignerSetup` therefore pins `mnemonic: DEFAULT_MNEMONIC` for dev mode and `resolveStorageSignerOptions` pins `storageSigner` to the dev bare-root (dev) or the `--suri` key (suri) — the bare-root carries its own Bulletin authorization on paseo-next-v2, and polkadot-app-deploy's committed-signer wrapper falls back to the shared pool if it ever lapses. Tests in `signerMode.test.ts`, `run.test.ts`, and `decentralize/run.test.ts` pin the contract. - **The "dev signer" used in dev mode is polkadot-app-deploy's `DEFAULT_MNEMONIC` bare-root account, not Substrate's `//Alice`.** The bare-root SS58 (`5DfhGyQd…`) is what polkadot-app-deploy uses internally for its DEFAULT_MNEMONIC storage + DotNS signing, so the CLI's `createDevPublishSigner` derives from the same `(mnemonic, path="")` pair via `seedToAccount`. Storage, DotNS, and registry publish all sign as one identity. Substrate's `//Alice` (`5Grwva…`) is a DIFFERENT account — `createDevSigner("Alice")` from `@parity/product-sdk-tx` returns that one. Don't mix them; the `signerModeAlice.test.ts` snapshot guards against regression. -- **Dev-mode re-publish only works on apps that were first published from dev mode.** `is_authorized_to_republish` accepts `caller == owner OR caller == publisher`. In dev mode the publisher is always Alice (`5DfhGyQd…`), so dev-mode re-deploys of a dev-published app succeed. But an app first published from phone mode has `caller == publisher == user H160`; Alice is neither, so a dev-mode re-deploy reverts `Unauthorized`. To iterate on a phone-published app in dev mode the user must unpublish it from phone mode first. Intentional asymmetry: once a user "owns" an app from their phone, a shared dev key can't touch it. +- **Dev-mode re-publish only works on apps that were first published from dev mode.** Re-publish authorization accepts `caller == owner OR caller == publisher`. Since v2 dropped the `owner` parameter, BOTH are just `env::caller()` at first publish — so a dev-mode deploy is owned+published by the dev bare-root (`5DfhGyQd…`) and a phone-mode deploy by the user's product H160. A dev-mode re-deploy of a phone-published app is therefore neither, and reverts `Unauthorized`; to iterate on it in dev mode the user must unpublish from phone mode first. Intentional asymmetry: once a user "owns" an app from their phone, a shared dev key can't touch it. Note this now cuts the other way too — an app you dev-deployed can't be claimed by your phone account later, because nothing can rewrite `owner`. - **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 polkadot-app-deploy's 300 s heartbeat with a one-off client that gets destroyed immediately after the upload. - **`playground deploy` does NOT pass `jsMerkle: true` today.** polkadot-app-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. `playground login` 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.3.x`'s `createSessionSignerForAccount` routes transaction signing through `session.createTransaction` — the wallet builds and signs the full extrinsic from a structured `ProductAccountTransaction`, 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. (History: 0.2.1 used `session.signRaw({ data: { tag: "Payload", … } })`; the pre-0.2.1 PJS path failed on v2 with `PJS does not support this signed-extension: AsPgas`.) diff --git a/cdm.json b/cdm.json index 97926bbe..bfc1c84d 100644 --- a/cdm.json +++ b/cdm.json @@ -1,214 +1,117 @@ { - "registry": "0xa5747e60ae27f93e92019e4021abfc4957050141", "dependencies": { - "@w3s/playground-registry": "latest" + "@w3s/playground-registry": "latest", + "@w3s/playground-identity": "latest", + "@w3s/playground-open-verifier": "latest" }, "contracts": { "@w3s/playground-registry": { - "version": 1, - "address": "0x49091Eb6dbBf2f95B55243b987ebc15eaaEe711F", + "version": 2, + "address": "0xF5E4E95c73a8F5Bc603b284D5fa2DE6cc86b7198", "abi": [ { - "type": "constructor", "inputs": [], - "stateMutability": "nonpayable" + "stateMutability": "nonpayable", + "type": "constructor" }, { - "type": "function", - "name": "spikeVerify", - "inputs": [ - { - "name": "signature", - "type": "uint8[]" - }, - { - "name": "message", - "type": "uint8[]" - }, - { - "name": "public_key", - "type": "bytes32" - } - ], + "inputs": [], + "name": "getAppCount", "outputs": [ { "name": "", - "type": "bool" + "type": "uint32" } ], - "stateMutability": "view" + "stateMutability": "view", + "type": "function" }, { - "type": "function", - "name": "publish", "inputs": [ { - "name": "domain", - "type": "string" - }, - { - "name": "metadata_uri", - "type": "string" - }, - { - "name": "visibility", - "type": "uint8" - }, - { - "name": "owner", - "type": "tuple", - "components": [ - { - "name": "isSome", - "type": "bool" - }, - { - "name": "value", - "type": "address" - } - ] - }, + "name": "index", + "type": "uint32" + } + ], + "name": "getDomainAt", + "outputs": [ { - "name": "modded_from", + "name": "", "type": "string" - }, - { - "name": "is_moddable", - "type": "bool" - }, - { - "name": "is_dev_signer", - "type": "bool" } ], - "outputs": [], - "stateMutability": "nonpayable" + "stateMutability": "view", + "type": "function" }, { - "type": "function", - "name": "publishDev", "inputs": [ { "name": "domain", "type": "string" - }, - { - "name": "metadata_uri", - "type": "string" - }, - { - "name": "visibility", - "type": "uint8" - }, - { - "name": "owner", - "type": "tuple", - "components": [ - { - "name": "isSome", - "type": "bool" - }, - { - "name": "value", - "type": "address" - } - ] - }, - { - "name": "modded_from", - "type": "string" - }, - { - "name": "is_moddable", - "type": "bool" } ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "unpublish", - "inputs": [ + "name": "getMetadataUri", + "outputs": [ { - "name": "domain", + "name": "", "type": "string" } ], - "outputs": [], - "stateMutability": "nonpayable" + "stateMutability": "view", + "type": "function" }, { - "type": "function", - "name": "star", "inputs": [ { "name": "domain", "type": "string" } ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "getAppCount", - "inputs": [], + "name": "getOwner", "outputs": [ { "name": "", - "type": "uint32" + "type": "address" } ], - "stateMutability": "view" + "stateMutability": "view", + "type": "function" }, { - "type": "function", - "name": "getDomainAt", "inputs": [ { - "name": "index", - "type": "uint32" + "name": "domain", + "type": "string" } ], + "name": "getVisibility", "outputs": [ { "name": "", - "type": "tuple", - "components": [ - { - "name": "isSome", - "type": "bool" - }, - { - "name": "value", - "type": "string" - } - ] + "type": "uint8" } ], - "stateMutability": "view" + "stateMutability": "view", + "type": "function" }, { - "type": "function", - "name": "getOwnerAppCount", "inputs": [ { "name": "owner", "type": "address" } ], + "name": "getOwnerAppCount", "outputs": [ { "name": "", "type": "uint32" } ], - "stateMutability": "view" + "stateMutability": "view", + "type": "function" }, { - "type": "function", - "name": "getOwnerDomainAt", "inputs": [ { "name": "owner", @@ -219,233 +122,253 @@ "type": "uint32" } ], + "name": "getOwnerDomainAt", "outputs": [ { "name": "", - "type": "tuple", - "components": [ - { - "name": "isSome", - "type": "bool" - }, - { - "name": "value", - "type": "string" - } - ] + "type": "string" } ], - "stateMutability": "view" + "stateMutability": "view", + "type": "function" }, { - "type": "function", - "name": "getSudo", "inputs": [], + "name": "getSudo", "outputs": [ { "name": "", "type": "address" } ], - "stateMutability": "view" + "stateMutability": "view", + "type": "function" }, { - "type": "function", - "name": "addAdmin", "inputs": [ { - "name": "admin", + "name": "addr", "type": "address" } ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "removeAdmin", - "inputs": [ + "name": "isAdmin", + "outputs": [ { - "name": "admin", - "type": "address" + "name": "", + "type": "bool" } ], - "outputs": [], - "stateMutability": "nonpayable" + "stateMutability": "view", + "type": "function" }, { - "type": "function", - "name": "isAdmin", "inputs": [ { - "name": "addr", + "name": "account", "type": "address" } ], + "name": "isBlacklisted", "outputs": [ { "name": "", "type": "bool" } ], - "stateMutability": "view" + "stateMutability": "view", + "type": "function" }, { - "type": "function", - "name": "adminSetDeployAwardCount", "inputs": [ { "name": "account", "type": "address" - }, - { - "name": "count", - "type": "uint32" } ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "adminSetAppSocialCounts", - "inputs": [ - { - "name": "domain", - "type": "string" - }, - { - "name": "star_count", - "type": "uint32" - }, + "name": "getPoints", + "outputs": [ { - "name": "mod_count", - "type": "uint32" + "name": "", + "type": "uint128" } ], - "outputs": [], - "stateMutability": "nonpayable" + "stateMutability": "view", + "type": "function" }, { - "type": "function", - "name": "setBlacklisted", "inputs": [ { - "name": "accounts", - "type": "address[]" + "name": "start", + "type": "uint32" }, { - "name": "value", - "type": "bool" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "isBlacklisted", - "inputs": [ - { - "name": "account", - "type": "address" + "name": "count", + "type": "uint32" } ], + "name": "getApps", "outputs": [ { + "components": [ + { + "name": "total", + "type": "uint32" + }, + { + "name": "scanned", + "type": "uint32" + }, + { + "components": [ + { + "name": "index", + "type": "uint32" + }, + { + "name": "domain", + "type": "string" + }, + { + "name": "metadata_uri", + "type": "string" + }, + { + "name": "owner", + "type": "address" + }, + { + "name": "visibility", + "type": "uint8" + }, + { + "name": "publisher", + "type": "address" + } + ], + "name": "entries", + "type": "tuple[]" + } + ], "name": "", - "type": "bool" + "type": "tuple" } ], - "stateMutability": "view" + "stateMutability": "view", + "type": "function" }, { - "type": "function", - "name": "setVisibility", "inputs": [ { "name": "domain", "type": "string" - }, + } + ], + "name": "getStarCount", + "outputs": [ { - "name": "visibility", - "type": "uint8" + "name": "", + "type": "uint32" } ], - "outputs": [], - "stateMutability": "nonpayable" + "stateMutability": "view", + "type": "function" }, { - "type": "function", - "name": "getVisibility", "inputs": [ { "name": "domain", "type": "string" } ], + "name": "getModCount", "outputs": [ { "name": "", - "type": "uint8" + "type": "uint32" } ], - "stateMutability": "view" + "stateMutability": "view", + "type": "function" }, { - "type": "function", - "name": "pin", - "inputs": [ + "inputs": [], + "name": "getLineageCount", + "outputs": [ { - "name": "domain", - "type": "string" + "name": "", + "type": "uint32" } ], - "outputs": [], - "stateMutability": "nonpayable" + "stateMutability": "view", + "type": "function" }, { - "type": "function", - "name": "unpin", "inputs": [ { - "name": "domain", - "type": "string" - } + "name": "start", + "type": "uint32" + }, + { + "name": "count", + "type": "uint32" + } ], - "outputs": [], - "stateMutability": "nonpayable" + "name": "getLineage", + "outputs": [ + { + "components": [ + { + "name": "child", + "type": "string" + }, + { + "name": "source", + "type": "string" + } + ], + "name": "", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" }, { - "type": "function", - "name": "isPinned", "inputs": [ + { + "name": "voter", + "type": "address" + }, { "name": "domain", "type": "string" } ], + "name": "hasStarred", "outputs": [ { "name": "", "type": "bool" } ], - "stateMutability": "view" + "stateMutability": "view", + "type": "function" }, { - "type": "function", - "name": "getPinnedApps", - "inputs": [], + "inputs": [ + { + "name": "domains", + "type": "string[]" + }, + { + "name": "voter", + "type": "address" + } + ], + "name": "getAppData", "outputs": [ { - "name": "", - "type": "tuple[]", "components": [ - { - "name": "index", - "type": "uint32" - }, { "name": "domain", "type": "string" @@ -465,137 +388,126 @@ { "name": "publisher", "type": "address" + }, + { + "name": "star_count", + "type": "uint32" + }, + { + "name": "mod_count", + "type": "uint32" + }, + { + "name": "has_starred", + "type": "bool" } - ] + ], + "name": "", + "type": "tuple[]" } ], - "stateMutability": "view" + "stateMutability": "view", + "type": "function" }, { - "type": "function", - "name": "getApps", "inputs": [ { - "name": "start", - "type": "uint32" - }, - { - "name": "count", - "type": "uint32" + "name": "account", + "type": "address" } ], + "name": "getPointBreakdown", "outputs": [ { - "name": "", - "type": "tuple", "components": [ { - "name": "total", - "type": "uint32" + "name": "mod_points", + "type": "uint128" }, { - "name": "scanned", - "type": "uint32" + "name": "star_points", + "type": "uint128" }, { - "name": "entries", - "type": "tuple[]", - "components": [ - { - "name": "index", - "type": "uint32" - }, - { - "name": "domain", - "type": "string" - }, - { - "name": "metadata_uri", - "type": "string" - }, - { - "name": "owner", - "type": "address" - }, - { - "name": "visibility", - "type": "uint8" - }, - { - "name": "publisher", - "type": "address" - } - ] + "name": "total", + "type": "uint128" } - ] + ], + "name": "", + "type": "tuple" } ], - "stateMutability": "view" + "stateMutability": "view", + "type": "function" }, { - "type": "function", - "name": "getTopStarred", "inputs": [ { - "name": "start", - "type": "uint32" - }, - { - "name": "count", - "type": "uint32" + "name": "domain", + "type": "string" } ], + "name": "isPinned", "outputs": [ { "name": "", - "type": "tuple", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getPinnedApps", + "outputs": [ + { "components": [ { - "name": "total", + "name": "index", "type": "uint32" }, { - "name": "scanned", - "type": "uint32" + "name": "domain", + "type": "string" }, { - "name": "entries", - "type": "tuple[]", - "components": [ - { - "name": "index", - "type": "uint32" - }, - { - "name": "domain", - "type": "string" - }, - { - "name": "metadata_uri", - "type": "string" - }, - { - "name": "owner", - "type": "address" - }, - { - "name": "visibility", - "type": "uint8" - }, - { - "name": "publisher", - "type": "address" - } - ] + "name": "metadata_uri", + "type": "string" + }, + { + "name": "owner", + "type": "address" + }, + { + "name": "visibility", + "type": "uint8" + }, + { + "name": "publisher", + "type": "address" } - ] + ], + "name": "", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getBuilderCount", + "outputs": [ + { + "name": "", + "type": "uint32" } ], - "stateMutability": "view" + "stateMutability": "view", + "type": "function" }, { - "type": "function", - "name": "getTopModded", "inputs": [ { "name": "start", @@ -606,10 +518,9 @@ "type": "uint32" } ], + "name": "getBuilders", "outputs": [ { - "name": "", - "type": "tuple", "components": [ { "name": "total", @@ -620,357 +531,240 @@ "type": "uint32" }, { - "name": "entries", - "type": "tuple[]", "components": [ { - "name": "index", - "type": "uint32" - }, - { - "name": "domain", - "type": "string" - }, - { - "name": "metadata_uri", - "type": "string" - }, - { - "name": "owner", + "name": "account", "type": "address" }, { - "name": "visibility", - "type": "uint8" - }, - { - "name": "publisher", - "type": "address" + "name": "score", + "type": "uint128" } - ] + ], + "name": "entries", + "type": "tuple[]" } - ] + ], + "name": "", + "type": "tuple" } ], - "stateMutability": "view" + "stateMutability": "view", + "type": "function" }, { - "type": "function", - "name": "getLineageCount", - "inputs": [], - "outputs": [ + "inputs": [ { - "name": "", - "type": "uint32" + "name": "domain", + "type": "string" } ], - "stateMutability": "view" + "name": "pin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" }, { - "type": "function", - "name": "getLineage", "inputs": [ { - "name": "start", - "type": "uint32" - }, - { - "name": "count", - "type": "uint32" - } - ], - "outputs": [ - { - "name": "", - "type": "tuple[]", - "components": [ - { - "name": "child", - "type": "string" - }, - { - "name": "source", - "type": "string" - } - ] + "name": "domain", + "type": "string" } ], - "stateMutability": "view" + "name": "unpin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" }, { - "type": "function", - "name": "getMetadataUri", "inputs": [ { "name": "domain", "type": "string" } ], - "outputs": [ - { - "name": "", - "type": "tuple", - "components": [ - { - "name": "isSome", - "type": "bool" - }, - { - "name": "value", - "type": "string" - } - ] - } - ], - "stateMutability": "view" + "name": "star", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" }, { - "type": "function", - "name": "getAppData", "inputs": [ { - "name": "domains", - "type": "string[]" + "name": "domain", + "type": "string" }, { - "name": "voter", - "type": "address" - } - ], - "outputs": [ + "name": "metadata_uri", + "type": "string" + }, { - "name": "", - "type": "tuple[]", - "components": [ - { - "name": "domain", - "type": "string" - }, - { - "name": "metadata_uri", - "type": "tuple", - "components": [ - { - "name": "isSome", - "type": "bool" - }, - { - "name": "value", - "type": "string" - } - ] - }, - { - "name": "owner", - "type": "address" - }, - { - "name": "visibility", - "type": "uint8" - }, - { - "name": "publisher", - "type": "address" - }, - { - "name": "star_count", - "type": "uint32" - }, - { - "name": "mod_count", - "type": "uint32" - }, - { - "name": "has_starred", - "type": "bool" - } - ] + "name": "visibility", + "type": "uint8" + }, + { + "name": "modded_from", + "type": "string" } ], - "stateMutability": "view" + "name": "publish", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" }, { - "type": "function", - "name": "getOwner", "inputs": [ { "name": "domain", "type": "string" + }, + { + "name": "visibility", + "type": "uint8" } ], - "outputs": [ + "name": "setVisibility", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ { - "name": "", - "type": "address" + "name": "domain", + "type": "string" } ], - "stateMutability": "view" + "name": "unpublish", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" }, { - "type": "function", - "name": "getPoints", "inputs": [ { - "name": "account", + "name": "admin", "type": "address" } ], - "outputs": [ + "name": "addAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ { - "name": "", - "type": "uint128" + "name": "admin", + "type": "address" } ], - "stateMutability": "view" + "name": "removeAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" }, { - "type": "function", - "name": "getTopBuilders", "inputs": [ { - "name": "start", - "type": "uint32" + "name": "account", + "type": "address" }, { "name": "count", "type": "uint32" } ], - "outputs": [ - { - "name": "", - "type": "tuple[]", - "components": [ - { - "name": "account", - "type": "address" - }, - { - "name": "score", - "type": "uint128" - } - ] - } - ], - "stateMutability": "view" + "name": "adminSetDeployAwardCount", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" }, { - "type": "function", - "name": "getModCount", "inputs": [ { "name": "domain", "type": "string" - } - ], - "outputs": [ + }, { - "name": "", + "name": "star_count", "type": "uint32" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "getStarCount", - "inputs": [ - { - "name": "domain", - "type": "string" - } - ], - "outputs": [ + }, { - "name": "", + "name": "mod_count", "type": "uint32" } ], - "stateMutability": "view" + "name": "adminSetAppSocialCounts", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" }, { - "type": "function", - "name": "hasStarred", "inputs": [ { - "name": "voter", - "type": "address" + "name": "accounts", + "type": "address[]" }, { - "name": "domain", - "type": "string" - } - ], - "outputs": [ - { - "name": "", + "name": "value", "type": "bool" } ], - "stateMutability": "view" + "name": "setBlacklisted", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" }, { - "type": "function", - "name": "getPointBreakdown", "inputs": [ { - "name": "account", - "type": "address" + "name": "value", + "type": "bool" } ], + "name": "setFrozen", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "isFrozen", "outputs": [ { "name": "", - "type": "tuple", - "components": [ - { - "name": "launch_points", - "type": "uint128" - }, - { - "name": "mod_points", - "type": "uint128" - }, - { - "name": "star_points", - "type": "uint128" - }, - { - "name": "total", - "type": "uint128" - } - ] + "type": "bool" } ], - "stateMutability": "view" + "stateMutability": "view", + "type": "function" }, { - "type": "function", - "name": "setFrozen", "inputs": [ { - "name": "value", - "type": "bool" + "name": "verifier", + "type": "address" } ], + "name": "setVerifier", "outputs": [], - "stateMutability": "nonpayable" + "stateMutability": "nonpayable", + "type": "function" }, { - "type": "function", - "name": "isFrozen", "inputs": [], + "name": "getVerifier", "outputs": [ { "name": "", - "type": "bool" + "type": "address" } ], - "stateMutability": "view" + "stateMutability": "view", + "type": "function" }, { - "type": "function", - "name": "importApp", "inputs": [ { "name": "domain", @@ -991,22 +785,16 @@ { "name": "metadata_uri", "type": "string" - }, - { - "name": "is_moddable", - "type": "bool" } ], + "name": "importApp", "outputs": [], - "stateMutability": "nonpayable" + "stateMutability": "nonpayable", + "type": "function" }, { - "type": "function", - "name": "importApps", "inputs": [ { - "name": "apps", - "type": "tuple[]", "components": [ { "name": "domain", @@ -1027,36 +815,32 @@ { "name": "metadata_uri", "type": "string" - }, - { - "name": "is_moddable", - "type": "bool" } - ] + ], + "name": "apps", + "type": "tuple[]" } ], + "name": "importApps", "outputs": [], - "stateMutability": "nonpayable" + "stateMutability": "nonpayable", + "type": "function" }, { - "type": "function", - "name": "importPinned", "inputs": [ { "name": "domain", "type": "string" } ], + "name": "importPinned", "outputs": [], - "stateMutability": "nonpayable" + "stateMutability": "nonpayable", + "type": "function" }, { - "type": "function", - "name": "importLineage", "inputs": [ { - "name": "entries", - "type": "tuple[]", "components": [ { "name": "child", @@ -1066,19 +850,19 @@ "name": "source", "type": "string" } - ] + ], + "name": "entries", + "type": "tuple[]" } ], + "name": "importLineage", "outputs": [], - "stateMutability": "nonpayable" + "stateMutability": "nonpayable", + "type": "function" }, { - "type": "function", - "name": "importPoints", "inputs": [ { - "name": "entries", - "type": "tuple[]", "components": [ { "name": "account", @@ -1088,19 +872,19 @@ "name": "total", "type": "uint128" } - ] + ], + "name": "entries", + "type": "tuple[]" } ], + "name": "importPoints", "outputs": [], - "stateMutability": "nonpayable" + "stateMutability": "nonpayable", + "type": "function" }, { - "type": "function", - "name": "importSocialCounts", "inputs": [ { - "name": "entries", - "type": "tuple[]", "components": [ { "name": "domain", @@ -1114,101 +898,210 @@ "name": "mod_count", "type": "uint32" } - ] + ], + "name": "entries", + "type": "tuple[]" } ], + "name": "importSocialCounts", "outputs": [], - "stateMutability": "nonpayable" + "stateMutability": "nonpayable", + "type": "function" }, { - "type": "function", - "name": "importIdentities", - "inputs": [ - { - "name": "entries", - "type": "tuple[]", - "components": [ - { - "name": "account", - "type": "address" - }, - { - "name": "root_pubkey", - "type": "bytes32" - } - ] - } - ], - "outputs": [], - "stateMutability": "nonpayable" + "inputs": [], + "name": "NotRevealed", + "type": "error" }, { - "type": "function", - "name": "setIdentity", - "inputs": [ - { - "name": "root_pubkey", - "type": "bytes32" - }, - { - "name": "signature", - "type": "uint8[]" - } - ], - "outputs": [], - "stateMutability": "nonpayable" + "inputs": [], + "name": "CannotPinPrivateApp", + "type": "error" }, { - "type": "function", - "name": "claimAnonymousBonus", "inputs": [], - "outputs": [], - "stateMutability": "nonpayable" + "name": "AlreadyPinned", + "type": "error" }, { - "type": "function", - "name": "clearIdentity", "inputs": [], - "outputs": [], - "stateMutability": "nonpayable" + "name": "NotPinned", + "type": "error" }, { - "type": "function", - "name": "getRootAccount", - "inputs": [ - { - "name": "account", - "type": "address" - } - ], - "outputs": [ - { - "name": "", - "type": "bytes32" - } - ], - "stateMutability": "view" + "inputs": [], + "name": "DeployAwardCountTooHigh", + "type": "error" + }, + { + "inputs": [], + "name": "Unauthorized", + "type": "error" + }, + { + "inputs": [], + "name": "Frozen", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidVisibility", + "type": "error" + }, + { + "inputs": [], + "name": "EmptyMetadataUri", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidModdedFrom", + "type": "error" + }, + { + "inputs": [], + "name": "AppNotFound", + "type": "error" + }, + { + "inputs": [], + "name": "SelfStarForbidden", + "type": "error" + }, + { + "inputs": [], + "name": "AlreadyStarred", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidCalldata", + "type": "error" + }, + { + "inputs": [], + "name": "CalldataTooLarge", + "type": "error" + }, + { + "inputs": [], + "name": "NoSelector", + "type": "error" + }, + { + "inputs": [], + "name": "UnknownSelector", + "type": "error" + }, + { + "inputs": [], + "name": "NonPayableValueReceived", + "type": "error" + } + ], + "metadataCid": "bafk2bzaceaik3v2d427yxjnvd646u67ppes6q3bn7srirucljce2ovlk4wgao" + }, + "@w3s/playground-identity": { + "version": 2, + "address": "0x5ac7a2F1f0d3f162189bF00Cbd8c4c1aD319057d", + "abi": [ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "name": "root_pubkey", + "type": "bytes32" + }, + { + "name": "signature", + "type": "uint8[]" + } + ], + "name": "setIdentity", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "clearIdentity", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "name": "account", + "type": "address" + } + ], + "name": "isVerified", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "name": "account", + "type": "address" + } + ], + "name": "rootOf", + "outputs": [ + { + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "name": "account", + "type": "address" + } + ], + "name": "getRootAccount", + "outputs": [ + { + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" }, { - "type": "function", - "name": "getRootAccounts", "inputs": [ { "name": "accounts", "type": "address[]" } ], + "name": "getRootAccounts", "outputs": [ { "name": "", "type": "bytes32[]" } ], - "stateMutability": "view" + "stateMutability": "view", + "type": "function" }, { - "type": "function", - "name": "adminSetIdentity", "inputs": [ { "name": "account", @@ -1219,30 +1112,325 @@ "type": "bytes32" } ], + "name": "adminSetIdentity", "outputs": [], - "stateMutability": "nonpayable" + "stateMutability": "nonpayable", + "type": "function" }, { - "type": "function", - "name": "adminClearIdentity", "inputs": [ { "name": "account", "type": "address" } ], + "name": "adminClearIdentity", "outputs": [], - "stateMutability": "nonpayable" + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "name": "admin", + "type": "address" + } + ], + "name": "addAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "name": "admin", + "type": "address" + } + ], + "name": "removeAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "name": "value", + "type": "bool" + } + ], + "name": "setFrozen", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "name": "accounts", + "type": "address[]" + }, + { + "name": "roots", + "type": "bytes32[]" + } + ], + "name": "importIdentities", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "IdentitySigLen", + "type": "error" + }, + { + "inputs": [], + "name": "IdentityRootZero", + "type": "error" }, { - "type": "function", - "name": "faucet", "inputs": [], + "name": "IdentitySigInvalid", + "type": "error" + }, + { + "inputs": [], + "name": "IdentityRootTaken", + "type": "error" + }, + { + "inputs": [], + "name": "Unauthorized", + "type": "error" + }, + { + "inputs": [], + "name": "Frozen", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidCalldata", + "type": "error" + }, + { + "inputs": [], + "name": "CalldataTooLarge", + "type": "error" + }, + { + "inputs": [], + "name": "NoSelector", + "type": "error" + }, + { + "inputs": [], + "name": "UnknownSelector", + "type": "error" + }, + { + "inputs": [], + "name": "NonPayableValueReceived", + "type": "error" + } + ], + "metadataCid": "bafk2bzacebyvwwyklujqsnsyvajbanxzkxcqebokcfud7aguhp265ckysc2q4" + }, + "@w3s/playground-open-verifier": { + "version": 2, + "address": "0x904c2BFc3d38F2FA5E755c95CEaA4929F8a23255", + "abi": [ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "name": "_account", + "type": "address" + } + ], + "name": "isVerified", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "name": "account", + "type": "address" + } + ], + "name": "rootOf", + "outputs": [ + { + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "name": "account", + "type": "address" + } + ], + "name": "getRootAccount", + "outputs": [ + { + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "name": "accounts", + "type": "address[]" + } + ], + "name": "getRootAccounts", + "outputs": [ + { + "name": "", + "type": "bytes32[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "faucet", "outputs": [], - "stateMutability": "nonpayable" + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "name": "account", + "type": "address" + } + ], + "name": "canClaim", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "name": "account", + "type": "address" + } + ], + "name": "lastClaimAt", + "outputs": [ + { + "name": "", + "type": "uint32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "name": "value", + "type": "bool" + } + ], + "name": "setFrozen", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "getSudo", + "outputs": [ + { + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "isFrozen", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "Unauthorized", + "type": "error" + }, + { + "inputs": [], + "name": "Frozen", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidCalldata", + "type": "error" + }, + { + "inputs": [], + "name": "CalldataTooLarge", + "type": "error" + }, + { + "inputs": [], + "name": "NoSelector", + "type": "error" + }, + { + "inputs": [], + "name": "UnknownSelector", + "type": "error" + }, + { + "inputs": [], + "name": "NonPayableValueReceived", + "type": "error" } ], - "metadataCid": "bafk2bzaceciehmxltebki5zxj4p7i476dnbn4yxeuisja76ko4shbjd6op5g2" + "metadataCid": "bafk2bzaceasznmzat57hb3igidwwxkhmh7yyvr5ua37yuxb34n7q5dwugjvd6" } - } + }, + "registry": "0xc1a73a4f93fde65b1cb1680baead248073566cb0" } diff --git a/src/commands/deploy/DeployScreen.tsx b/src/commands/deploy/DeployScreen.tsx index c3393264..09b68fd6 100644 --- a/src/commands/deploy/DeployScreen.tsx +++ b/src/commands/deploy/DeployScreen.tsx @@ -876,7 +876,7 @@ function ConfirmStage({ : inputs.mode === "phone" || userSigner?.source === "dev" ? userSigner?.address : undefined, - claimedOwnerH160: "claimedOwnerH160" in setup ? setup.claimedOwnerH160 : undefined, + userSessionH160: "userSessionH160" in setup ? setup.userSessionH160 : undefined, }); useInput((_input, key) => { diff --git a/src/commands/deploy/index.ts b/src/commands/deploy/index.ts index 5efcab66..20b5943e 100644 --- a/src/commands/deploy/index.ts +++ b/src/commands/deploy/index.ts @@ -582,7 +582,7 @@ async function runHeadless(ctx: { (mode === "phone" || ctx.userSigner?.source === "dev" ? ctx.userSigner?.address : undefined), - claimedOwnerH160: setup.claimedOwnerH160, + userSessionH160: setup.userSessionH160, }); process.stdout.write("\n" + renderSummaryText(view) + "\n"); diff --git a/src/commands/deploy/summary.test.ts b/src/commands/deploy/summary.test.ts index a4226fd9..5042e7e0 100644 --- a/src/commands/deploy/summary.test.ts +++ b/src/commands/deploy/summary.test.ts @@ -65,16 +65,17 @@ describe("buildSummaryView", () => { }); expect(view.totalApprovals).toBe(0); expect(view.approvalLines).toEqual([]); - // Without claimedOwnerH160, no "App owner" row is added. + // Without userSessionH160, no "App owner" row is added. expect(view.rows.find((r) => r.label === "App owner")).toBeUndefined(); }); - it("dev mode with playground surfaces the claimed owner row when a session H160 is set", () => { - // Headline scenario: user did `dot login`, chose dev signer mode. - // The summary must tell them which H160 will be recorded as the - // app owner so they can trust that MyApps will resolve their app - // — without this row, the user sees "0 phone taps" and a blank - // owner with no way to verify their identity will land on chain. + it("dev mode with playground warns that the DEV signer owns the app, not the user", () => { + // Registry #525 removed `publish`'s owner argument, so a dev-mode + // deploy records the DEV signer as owner — the user's app will not + // appear in their MyApps. This row previously promised the opposite + // ("your account (0x…)"), which is false against the deployed v2 + // registry. It must warn, and must NOT echo the user's H160 as if it + // were going on chain. const view = buildSummaryView({ mode: "dev", domain: "my-app.dot", @@ -82,11 +83,13 @@ describe("buildSummaryView", () => { skipBuild: false, publishToPlayground: true, approvals: [], - claimedOwnerH160: "0xbeefbeefbeefbeefbeefbeefbeefbeefbeefbeef", + userSessionH160: "0xbeefbeefbeefbeefbeefbeefbeefbeefbeefbeef", }); expect(view.totalApprovals).toBe(0); const ownerRow = view.rows.find((r) => r.label === "App owner"); - expect(ownerRow?.value).toContain("0xbeefbeef"); + expect(ownerRow?.value).toContain("not your account"); + // The user's H160 must not be presented as the recorded owner. + expect(ownerRow?.value).not.toContain("0xbeefbeef"); }); it("shows the chosen tag in the Tag row when publishing to playground", () => { diff --git a/src/commands/deploy/summary.ts b/src/commands/deploy/summary.ts index d71cb954..c87b375b 100644 --- a/src/commands/deploy/summary.ts +++ b/src/commands/deploy/summary.ts @@ -48,7 +48,7 @@ export interface SummaryInputs { * user's session H160. When present, the summary surfaces it so the * user knows the app will still appear in their MyApps view. */ - claimedOwnerH160?: string | null; + userSessionH160?: string | null; } export interface SummaryView { @@ -98,15 +98,17 @@ export function buildSummaryView(input: SummaryInputs): SummaryView { value: input.moddable ? `yes — ${input.repositoryUrl}` : "no", }); rows.push({ label: "Tag", value: input.tag ? input.tag : "none" }); - if (input.claimedOwnerH160) { - // Dev mode + session: Alice signs the registry tx but the - // user's H160 is recorded as owner. Surfacing it here is - // what the spec promises: "Signing as Alice (dev). Your - // account will be recorded as the app owner so the app - // shows in MyApps." + // Registry #525 dropped `publish`'s owner argument: the CALLER is + // always recorded as owner, and no path can name another account. In + // dev mode that is the shared dev signer, NOT the logged-in user — so + // the app will not appear in their MyApps. This row used to promise the + // opposite ("your account (0x…)"), which is now false on-chain; verified + // against the deployed v2 registry, where a dev-mode publish recorded + // the dev H160. Say who will actually own it instead of hiding it. + if (input.mode === "dev" && input.userSessionH160) { rows.push({ label: "App owner", - value: `your account (${input.claimedOwnerH160})`, + value: "the dev signer — not your account (won't show in MyApps)", }); } } diff --git a/src/commands/mod/SetupScreen.tsx b/src/commands/mod/SetupScreen.tsx index 0738595c..32dffa96 100644 --- a/src/commands/mod/SetupScreen.tsx +++ b/src/commands/mod/SetupScreen.tsx @@ -40,6 +40,7 @@ import { Select } from "../../utils/ui/theme/Select.js"; import { VERSION_LABEL } from "../../utils/version.js"; import { getEnvTld, getNetworkLabel } from "../../config.js"; import { fetchBulletinJson, getBulletinGateway } from "../../utils/bulletinGateway.js"; +import { getAppMetadataUri } from "../../utils/mod/metadataUri.js"; interface AppMetadata { name?: string; @@ -94,13 +95,7 @@ export function SetupScreen({ domain, metadata: initial, registry, targetDir, on return; } log(`querying registry for ${domain}...`); - const metaRes = await registry.getMetadataUri.query(domain); - if (!metaRes.success) { - throw new Error( - `Registry lookup for "${domain}" failed at dry-run (chain rejected the call).`, - ); - } - const cid = metaRes.value.isSome ? metaRes.value.value : null; + const cid = await getAppMetadataUri(registry, domain); if (!cid) throw new Error(`App "${domain}" not found in registry`); log(`fetching metadata from IPFS (${cid.slice(0, 16)}...)...`); diff --git a/src/commands/mod/index.ts b/src/commands/mod/index.ts index 9003f8d0..011cb874 100644 --- a/src/commands/mod/index.ts +++ b/src/commands/mod/index.ts @@ -30,6 +30,7 @@ import { QuestPicker } from "./QuestPicker.js"; import { defaultRepoName } from "../../utils/git/repoName.js"; import { runCliCommand } from "../../cli-runtime.js"; import { parseGitHubRepoUrl, type GitHubRepoRef } from "../../utils/mod/source.js"; +import { getAppMetadataUri, type MetadataUriReader } from "../../utils/mod/metadataUri.js"; import { fetchBulletinJson, getBulletinGateway } from "../../utils/bulletinGateway.js"; import { editWithAgentStep } from "./nextSteps.js"; import { shouldShowTutorialPrompt } from "./tutorialPromptHint.js"; @@ -227,14 +228,11 @@ async function resolveTargetDir(args: { domain: string }): Promise { - const metaRes = await registry.getMetadataUri.query(domain); - if (!metaRes.success) { - throw new Error( - `Registry lookup for "${domain}" failed at dry-run (chain rejected the call)`, - ); - } - const cid = metaRes.value.isSome ? metaRes.value.value : null; +async function fetchAppMetadata( + registry: MetadataUriReader, + domain: string, +): Promise { + const cid = await getAppMetadataUri(registry, domain); if (!cid) throw new Error(`App "${domain}" not found in registry`); return await fetchBulletinJson(cid, getBulletinGateway()); } diff --git a/src/utils/decentralize/run.test.ts b/src/utils/decentralize/run.test.ts index cfbf0dff..cc6cb2f6 100644 --- a/src/utils/decentralize/run.test.ts +++ b/src/utils/decentralize/run.test.ts @@ -231,7 +231,6 @@ describe("runDecentralize — playground publish metadata", () => { type PublishArg = { repositoryUrl: string | null; - isModdable?: boolean; cwd?: string; }; @@ -250,7 +249,6 @@ describe("runDecentralize — playground publish metadata", () => { expect(publishToPlaygroundMock).toHaveBeenCalledTimes(1); const arg = publishToPlaygroundMock.mock.calls[0][0] as PublishArg; expect(arg.repositoryUrl).toBe("https://github.com/acme/site"); - expect(arg.isModdable).toBe(true); // cwd is the resolved git repo root (walked up from the typed --path), // not the build dir — publishToPlayground inlines the *project* README // as the app's detail page, matching how the moddable origin resolves. @@ -272,11 +270,10 @@ describe("runDecentralize — playground publish metadata", () => { const arg = publishToPlaygroundMock.mock.calls[0][0] as PublishArg; expect(arg.repositoryUrl).toBeNull(); - expect(arg.isModdable).toBe(false); expect(arg.cwd).toBe("./dist/__repo_root__"); }); - it("url source records no repository, no moddable bit, and no project root", async () => { + it("url source records no repository and no project root", async () => { // Mirrored sites have no git source: even if a caller smuggled a repo // URL in, the contract for url mode is null/false/undefined — pinned // here without the smuggling (callers can't reach the option in url @@ -293,7 +290,6 @@ describe("runDecentralize — playground publish metadata", () => { const arg = publishToPlaygroundMock.mock.calls[0][0] as PublishArg; expect(arg.repositoryUrl).toBeNull(); - expect(arg.isModdable).toBe(false); expect(arg.cwd).toBeUndefined(); }); }); diff --git a/src/utils/decentralize/run.ts b/src/utils/decentralize/run.ts index 4186349a..9c7e8e75 100644 --- a/src/utils/decentralize/run.ts +++ b/src/utils/decentralize/run.ts @@ -145,7 +145,7 @@ export interface RunDecentralizeOptions { * — git origin exists, public, GitHub) before passing it in; the runner * just threads it through. Only meaningful for `path` sources — mirrored * URL sites have no git source, so URL-mode callers always pass - * null/omit, and `isModdable` stays false. + * null/omit, and the app records no repository (not moddable). */ repositoryUrl?: string | null; env: Env; @@ -198,7 +198,7 @@ export async function runDecentralize( // signs with a dev key (polkadot-app-deploy default mnemonic or `--suri`). // This drives the "owned by a development account" callout — which speaks // to DotNS *domain* ownership (dev-signed in dev mode regardless of any - // registry-level `claimedOwnerH160`). + // registry-level owner override — #525 removed that capability). const storageSignerSource: ResolvedSigner["source"] = mode === "phone" ? "session" : "dev"; // Shared counter across every phone tap (DotNS commitment/finalize/link, @@ -320,7 +320,6 @@ export async function runDecentralize( const publishResult = await publishToPlayground({ domain: label, publishSigner, - claimedOwnerH160: setup.claimedOwnerH160, repositoryUrl, tag: options.tag ?? null, // Path sources have a real project root — its README.md (if @@ -332,8 +331,6 @@ export async function runDecentralize( cwd: source.kind === "path" ? findProjectRoot(source.directory) : undefined, env, isPrivate: false, - isModdable: repositoryUrl !== null, - isDevSigner: setup.publishSigner.source === "dev", onLogEvent: (event) => onEvent?.({ kind: "playground-event", event }), onAllowancePrompt: allowancePrompt, }); diff --git a/src/utils/deploy/playground.test.ts b/src/utils/deploy/playground.test.ts index ae7b9fa9..489872c2 100644 --- a/src/utils/deploy/playground.test.ts +++ b/src/utils/deploy/playground.test.ts @@ -548,18 +548,7 @@ describe("publishToPlayground", () => { expect.objectContaining({ __kind: "store" }), bulletinStorageSigner, ); - expect(publishTx).toHaveBeenCalledWith( - "my-app.paseo", - "bafymeta", - 1, - { - isSome: false, - value: "0x0000000000000000000000000000000000000000", - }, - "", - false, - false, - ); + expect(publishTx).toHaveBeenCalledWith("my-app.paseo", "bafymeta", 1, ""); } finally { rmSync(dir, { recursive: true, force: true }); } @@ -621,27 +610,14 @@ describe("publishToPlayground", () => { } }); - it("passes claimedOwnerH160 through as the dev-signer publish owner argument", async () => { + it("cannot name an owner — #525 made the caller the owner unconditionally", async () => { await publishToPlayground({ domain: "claimed-app", publishSigner: fakeSigner, repositoryUrl: null, cwd: "/definitely/not/a/repo", - claimedOwnerH160: "0x1234567890abcdef1234567890abcdef12345678", - isDevSigner: true, }); - expect(publishTx).toHaveBeenCalledWith( - "claimed-app.paseo", - "bafymeta", - 1, - { - isSome: true, - value: "0x1234567890abcdef1234567890abcdef12345678", - }, - "", - false, - true, - ); + expect(publishTx).toHaveBeenCalledWith("claimed-app.paseo", "bafymeta", 1, ""); }); it("passes visibility=0 when isPrivate is true", async () => { @@ -652,41 +628,17 @@ describe("publishToPlayground", () => { cwd: "/definitely/not/a/repo", isPrivate: true, }); - expect(publishTx).toHaveBeenCalledWith( - "secret.paseo", - "bafymeta", - 0, - { - isSome: false, - value: "0x0000000000000000000000000000000000000000", - }, - "", - false, - false, - ); + expect(publishTx).toHaveBeenCalledWith("secret.paseo", "bafymeta", 0, ""); }); - it("routes dev signer publishes through publish with is_dev_signer=true", async () => { + it("publishes with four args only — #525 dropped owner/is_moddable/is_dev_signer", async () => { await publishToPlayground({ domain: "modded-by-dev", publishSigner: fakeSigner, repositoryUrl: "https://github.com/foo/bar", cwd: "/definitely/not/a/repo", - isModdable: true, - isDevSigner: true, }); - expect(publishTx).toHaveBeenCalledWith( - "modded-by-dev.paseo", - "bafymeta", - 1, - { - isSome: false, - value: "0x0000000000000000000000000000000000000000", - }, - "", - true, - true, - ); + expect(publishTx).toHaveBeenCalledWith("modded-by-dev.paseo", "bafymeta", 1, ""); }); it("forwards moddedFrom captured by `dot mod` in dot.json to registry.publish", async () => { @@ -699,18 +651,7 @@ describe("publishToPlayground", () => { repositoryUrl: null, cwd: dir, }); - expect(publishTx).toHaveBeenCalledWith( - "my-mod.paseo", - "bafymeta", - 1, - { - isSome: false, - value: "0x0000000000000000000000000000000000000000", - }, - "original.paseo", - false, - false, - ); + expect(publishTx).toHaveBeenCalledWith("my-mod.paseo", "bafymeta", 1, "original.paseo"); } finally { rmSync(dir, { recursive: true, force: true }); } @@ -741,13 +682,7 @@ describe("publishToPlayground", () => { "my-mod.paseo", "bafymeta", 1, - { - isSome: false, - value: "0x0000000000000000000000000000000000000000", - }, "steampunk-lizard-spock01.paseo", - false, - false, ); // The explicit value must ALSO drive the metadata JSON, not just the // on-chain arg — otherwise the badge and the XP edge disagree. @@ -775,13 +710,7 @@ describe("publishToPlayground", () => { "my-mod.paseo", "bafymeta", 1, - { - isSome: false, - value: "0x0000000000000000000000000000000000000000", - }, "steampunk-lizard-spock01.paseo", - false, - false, ); expect(result.metadata.moddedFrom).toBe("steampunk-lizard-spock01.paseo"); } finally { @@ -800,18 +729,7 @@ describe("publishToPlayground", () => { cwd: dir, moddedFrom: " ", }); - expect(publishTx).toHaveBeenCalledWith( - "my-mod.paseo", - "bafymeta", - 1, - { - isSome: false, - value: "0x0000000000000000000000000000000000000000", - }, - "original.paseo", - false, - false, - ); + expect(publishTx).toHaveBeenCalledWith("my-mod.paseo", "bafymeta", 1, "original.paseo"); expect(result.metadata.moddedFrom).toBe("original.paseo"); } finally { rmSync(dir, { recursive: true, force: true }); @@ -832,18 +750,7 @@ describe("publishToPlayground", () => { cwd: dir, moddedFrom: "Not A Domain!", }); - expect(publishTx).toHaveBeenCalledWith( - "my-mod.paseo", - "bafymeta", - 1, - { - isSome: false, - value: "0x0000000000000000000000000000000000000000", - }, - "original.paseo", - false, - false, - ); + expect(publishTx).toHaveBeenCalledWith("my-mod.paseo", "bafymeta", 1, "original.paseo"); expect(result.metadata.moddedFrom).toBe("original.paseo"); } finally { rmSync(dir, { recursive: true, force: true }); diff --git a/src/utils/deploy/playground.ts b/src/utils/deploy/playground.ts index 9820ee27..ec629ce5 100644 --- a/src/utils/deploy/playground.ts +++ b/src/utils/deploy/playground.ts @@ -22,7 +22,8 @@ * `getRegistryContract()`. * Phone publishing is signed by the user's product account so the contract's * `env::caller()` matches their address. Dev publishing is signed by the dev - * account and may pass a claimed owner for the playground-app "myApps" view. + * account and is therefore itself the recorded owner (registry #525 removed + * the `owner` argument, so no path can name another account). * * We deliberately do NOT use `polkadot-app-deploy.deploy()` for the metadata * upload: `deploy()` unconditionally runs a DotNS `register()` + @@ -113,20 +114,11 @@ export interface PublishToPlaygroundOptions { /** * Signer that submits the registry publish tx. In phone mode this is the * user's session signer and calls `publish(...)` (caller becomes owner). - * In dev mode this is a dev signer (Alice / `--suri`) and calls - * `publish(...)` with `is_dev_signer: true`; `claimedOwnerH160` carries the - * H160 to record as owner. + * In dev mode this is a dev signer (Alice / `--suri`) — and since registry + * #525 the caller is ALWAYS the recorded owner, so a dev-mode publish is + * owned by the dev account, not the logged-in user. */ publishSigner: ResolvedSigner; - /** - * Optional H160 to record as the app owner via the contract's `owner` - * parameter. Used by dev mode + active session so the app shows in the - * user's MyApps view even though the tx is signed by Alice. `null` or - * omitted ⇒ contract defaults to caller (`publishSigner.address` - * translated to H160), which is correct for phone mode and pure-dev - * throwaway. - */ - claimedOwnerH160?: `0x${string}` | null; /** Repository URL to record in metadata. `null` = omit the field entirely. */ repositoryUrl: string | null; /** @@ -160,7 +152,6 @@ export interface PublishToPlaygroundOptions { * records this bit so the playground-app filter doesn't need to fetch * each metadata JSON to know. */ - isModdable?: boolean; /** * Domain (`