diff --git a/.changeset/registry-username-display.md b/.changeset/registry-username-display.md new file mode 100644 index 0000000..f9cfd0e --- /dev/null +++ b/.changeset/registry-username-display.md @@ -0,0 +1,17 @@ +--- +"playground-cli": patch +--- + +`dot init` now shows the user's registry username (the handle set on the +playground.dot profile) when one has been claimed, falling back to the +People-parachain identity name and then to the H160, same precedence as +the playground-app. Also surfaces an "account in use" row with the +derivation path + H160 so the user can verify the exact account that +signs on their behalf. + +`dot deploy --playground` now matches the v11 registry contract's 7-arg +`publish()` signature (adds `modded_from`, `is_moddable`, `is_dev_signer`), +which unblocks publishes against the freshly deployed playground registry +on Paseo Asset Hub Next. `cdm.json` is refreshed to the v11 manifest; the +runtime keeps resolving the live contract address from the on-chain +meta-registry. diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 9c3d88e..3e415ad 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -132,11 +132,18 @@ jobs: run: | sudo apt-get update -q sudo apt-get install -y -q --no-install-recommends build-essential pkg-config - if ! command -v rustup >/dev/null 2>&1; then - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal - fi + # Parity self-hosted runners ship rustup on PATH at a + # non-standard location (not $HOME/.cargo). The previous + # guarded install left $HOME/.cargo/env missing, then + # `source` aborted the step under `set -e`. Run rustup-init + # unconditionally (it's idempotent and `-y` suppresses + # prompts), and source the env file only if it exists. + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain none echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" - source "$HOME/.cargo/env" + if [ -f "$HOME/.cargo/env" ]; then + # shellcheck disable=SC1091 + source "$HOME/.cargo/env" + fi rustup toolchain install nightly --profile minimal --component rust-src rustup default nightly curl -fsSL https://raw.githubusercontent.com/paritytech/contract-dependency-manager/main/install.sh | bash diff --git a/cdm.json b/cdm.json index 566536d..5bd5257 100644 --- a/cdm.json +++ b/cdm.json @@ -14,8 +14,8 @@ "contracts": { "929b5c63e2cb5202": { "@w3s/playground-registry": { - "version": 7, - "address": "0xea3fb6C5cDA79FEEf5b2d42a9122fC1BAFaF647F", + "version": 11, + "address": "0x109BBf68F41B570Fd6d3b7bDE9b8e18779FDd8Db", "abi": [ { "type": "constructor", @@ -51,6 +51,18 @@ "type": "address" } ] + }, + { + "name": "modded_from", + "type": "string" + }, + { + "name": "is_moddable", + "type": "bool" + }, + { + "name": "is_dev_signer", + "type": "bool" } ], "outputs": [], @@ -104,6 +116,30 @@ "outputs": [], "stateMutability": "nonpayable" }, + { + "type": "function", + "name": "star", + "inputs": [ + { + "name": "domain", + "type": "string" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "unstar", + "inputs": [ + { + "name": "domain", + "type": "string" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, { "type": "function", "name": "getContextId", @@ -256,6 +292,39 @@ ], "stateMutability": "view" }, + { + "type": "function", + "name": "setBlacklisted", + "inputs": [ + { + "name": "accounts", + "type": "address[]" + }, + { + "name": "value", + "type": "bool" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "isBlacklisted", + "inputs": [ + { + "name": "account", + "type": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "stateMutability": "view" + }, { "type": "function", "name": "setVisibility", @@ -473,6 +542,144 @@ ], "stateMutability": "view" }, + { + "type": "function", + "name": "getPoints", + "inputs": [ + { + "name": "account", + "type": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint128" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getTopBuilders", + "inputs": [ + { + "name": "start", + "type": "uint32" + }, + { + "name": "count", + "type": "uint32" + } + ], + "outputs": [ + { + "name": "", + "type": "tuple[]", + "components": [ + { + "name": "account", + "type": "address" + }, + { + "name": "score", + "type": "uint128" + } + ] + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getModCount", + "inputs": [ + { + "name": "domain", + "type": "string" + } + ], + "outputs": [ + { + "name": "", + "type": "uint32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getStarCount", + "inputs": [ + { + "name": "domain", + "type": "string" + } + ], + "outputs": [ + { + "name": "", + "type": "uint32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "hasStarred", + "inputs": [ + { + "name": "voter", + "type": "address" + }, + { + "name": "domain", + "type": "string" + } + ], + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getPointBreakdown", + "inputs": [ + { + "name": "account", + "type": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "tuple", + "components": [ + { + "name": "launch_points", + "type": "uint128" + }, + { + "name": "mod_points", + "type": "uint128" + }, + { + "name": "star_points", + "type": "uint128" + }, + { + "name": "total", + "type": "uint128" + } + ] + } + ], + "stateMutability": "view" + }, { "type": "function", "name": "setFrozen", @@ -527,6 +734,10 @@ { "name": "metadata_uri", "type": "string" + }, + { + "name": "is_moddable", + "type": "bool" } ], "outputs": [], @@ -559,6 +770,10 @@ { "name": "metadata_uri", "type": "string" + }, + { + "name": "is_moddable", + "type": "bool" } ] } @@ -577,9 +792,100 @@ ], "outputs": [], "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "setUsername", + "inputs": [ + { + "name": "name", + "type": "string" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "clearUsername", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "getUsername", + "inputs": [ + { + "name": "account", + "type": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "string" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getUsernames", + "inputs": [ + { + "name": "accounts", + "type": "address[]" + } + ], + "outputs": [ + { + "name": "", + "type": "string[]" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getUsernameOwner", + "inputs": [ + { + "name": "name", + "type": "string" + } + ], + "outputs": [ + { + "name": "", + "type": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "isUsernameAvailable", + "inputs": [ + { + "name": "name", + "type": "string" + }, + { + "name": "prospective_caller", + "type": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "stateMutability": "view" } ], - "metadataCid": "bafk2bzacedinnjvwctmltwri2xkmzhe26gt3ou7dimujsnwbinst3t33n2qgy" + "metadataCid": "bafk2bzaceatxst44simdeefvybqtrytkbsxhpktn7cjfhpuei6soggmped4oi" } } } diff --git a/src/commands/init/IdentityLines.tsx b/src/commands/init/IdentityLines.tsx index 7b767bc..999a504 100644 --- a/src/commands/init/IdentityLines.tsx +++ b/src/commands/init/IdentityLines.tsx @@ -16,47 +16,86 @@ import { useEffect, useState } from "react"; import { Row, Section } from "../../utils/ui/theme/index.js"; import type { SessionAddresses } from "../../utils/auth.js"; -import { formatUsernameLine, lookupUsername, type UsernameLookup } from "../../utils/username.js"; +import { + formatUsernameLine, + lookupUsername, + lookupRegistryUsername, + type UsernameLookup, +} from "../../utils/username.js"; +import { PLAYGROUND_PRODUCT_ID } from "../../config.js"; /** - * Three-line identity block shown after a successful login: + * Four-row identity block shown after a successful login: * - * logged in - * username alice.dot - * product account () + * logged in + * username (playground|polkadot) + * account in use playground.dot/0 — + * product account * * `logged in` is the SSO-handshake `rootAccountId` (bare-mnemonic on - * current mobile builds). It is the storage key for the username - * lookup. It is NOT the same address mobile shows as "Wallet account" - * on its debug screen — that uses the hard `//wallet` derivation which - * the host can't reproduce. + * current mobile builds). It is the storage key for the People-parachain + * username lookup. It is NOT the same address mobile shows as "Wallet + * account" on its debug screen — that uses the hard `//wallet` + * derivation which the host can't reproduce. * - * `product account` is the playground-scoped account derived via - * `product/playground.dot/0` off the root; this is what signs txs on - * the CLI. The SS58 + H160 are taken straight off the auth-derived - * pair so they never drift — the bug we had previously was running + * `username` follows a two-tier precedence, same first tier as the + * playground-app's `displayNameForAccount`: + * 1. registry username — the handle the user set in the playground-app + * profile (`registry.setUsername`), keyed on the product H160 since + * that's the `caller()` the contract records. + * 2. People-parachain identity from `lookupUsername` — the chain-wide + * handle, keyed on the root SS58. + * The source ("playground" vs "polkadot") is suffixed so it's obvious + * which surface the user is seeing. If neither resolves, the row falls + * through to `formatUsernameLine`'s `(no username set on chain)`. + * + * `account in use` surfaces the derivation slug + the H160 that signs + * on the user's behalf, so the user can verify the exact account + * without inspecting the SS58. `product account` keeps the SS58 form + * on its own row. + * + * Both lookups are async, fired in parallel, each cancellable. The + * People-parachain query has a 10s timeout inside `lookupUsername`; + * the registry query degrades silently to `null` on any error + * (`lookupRegistryUsername`) so older deploys without `getUsername` + * fall through to tier 2 without surfacing an error to the user. + * + * The SS58 + H160 are taken straight off the auth-derived pair so + * they never drift — the bug we had previously was running * `deriveProductAccountPublicKey` again on the already-derived SS58 * and producing a doubly-derived ghost address. - * - * The username lookup is async (queries People parachain) and has a - * 10s timeout inside `lookupUsername`. A `(looking up...)` placeholder - * renders while the lookup is in flight; failures and missing - * identities fall through to the strings from `formatUsernameLine`. */ export function IdentityLines({ addresses }: { addresses: SessionAddresses }) { - const [username, setUsername] = useState({ kind: "loading" }); + const [walletUsername, setWalletUsername] = useState({ kind: "loading" }); + // null means "lookup completed, no registry username set"; undefined means + // "still loading". Display rule: prefer registry > People > fall back to + // the H160 — same precedence the playground-app uses in `displayNameForAccount`. + const [registryUsername, setRegistryUsername] = useState(undefined); useEffect(() => { let cancelled = false; lookupUsername(addresses.rootAddress).then((result) => { - if (!cancelled) setUsername(result); + if (!cancelled) setWalletUsername(result); + }); + lookupRegistryUsername(addresses.productH160 as `0x${string}`).then((result) => { + if (!cancelled) setRegistryUsername(result); }); return () => { cancelled = true; }; - }, [addresses.rootAddress]); + }, [addresses.rootAddress, addresses.productH160]); - const usernameTone = username.kind === "found" ? "default" : "muted"; + const usernameLine = registryUsername + ? registryUsername + : registryUsername === undefined + ? "(looking up...)" + : formatUsernameLine(walletUsername); + const usernameTone = registryUsername || walletUsername.kind === "found" ? "default" : "muted"; + const usernameSource = registryUsername + ? "playground" + : walletUsername.kind === "found" + ? "polkadot" + : null; return (
@@ -64,15 +103,16 @@ export function IdentityLines({ addresses }: { addresses: SessionAddresses }) { +
); } diff --git a/src/utils/deploy/playground.test.ts b/src/utils/deploy/playground.test.ts index 37a54e8..87085ce 100644 --- a/src/utils/deploy/playground.test.ts +++ b/src/utils/deploy/playground.test.ts @@ -326,10 +326,18 @@ describe("publishToPlayground", () => { expect.objectContaining({ __kind: "store" }), bulletinStorageSigner, ); - expect(publishTx).toHaveBeenCalledWith("my-app.dot", "bafymeta", 1, { - isSome: false, - value: "0x0000000000000000000000000000000000000000", - }); + expect(publishTx).toHaveBeenCalledWith( + "my-app.dot", + "bafymeta", + 1, + { + isSome: false, + value: "0x0000000000000000000000000000000000000000", + }, + "", + false, + false, + ); } finally { rmSync(dir, { recursive: true, force: true }); } @@ -388,10 +396,18 @@ describe("publishToPlayground", () => { cwd: "/definitely/not/a/repo", claimedOwnerH160: "0x1234567890abcdef1234567890abcdef12345678", }); - expect(publishTx).toHaveBeenCalledWith("claimed-app.dot", "bafymeta", 1, { - isSome: true, - value: "0x1234567890abcdef1234567890abcdef12345678", - }); + expect(publishTx).toHaveBeenCalledWith( + "claimed-app.dot", + "bafymeta", + 1, + { + isSome: true, + value: "0x1234567890abcdef1234567890abcdef12345678", + }, + "", + false, + false, + ); }); it("passes visibility=0 when isPrivate is true", async () => { @@ -402,10 +418,41 @@ describe("publishToPlayground", () => { cwd: "/definitely/not/a/repo", isPrivate: true, }); - expect(publishTx).toHaveBeenCalledWith("secret.dot", "bafymeta", 0, { - isSome: false, - value: "0x0000000000000000000000000000000000000000", + expect(publishTx).toHaveBeenCalledWith( + "secret.dot", + "bafymeta", + 0, + { + isSome: false, + value: "0x0000000000000000000000000000000000000000", + }, + "", + false, + false, + ); + }); + + it("forwards isModdable and isDevSigner to registry.publish", 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.dot", + "bafymeta", + 1, + { + isSome: false, + value: "0x0000000000000000000000000000000000000000", + }, + "", + true, + true, + ); }); it("retries up to 3 times on registry publish failure", async () => { diff --git a/src/utils/deploy/playground.ts b/src/utils/deploy/playground.ts index 7ba2916..59e8c5d 100644 --- a/src/utils/deploy/playground.ts +++ b/src/utils/deploy/playground.ts @@ -83,6 +83,27 @@ export interface PublishToPlaygroundOptions { * to its owner in the playground. Defaults to public (visibility=1). */ isPrivate?: boolean; + /** + * Whether the published source is moddable (a public GitHub origin is + * recorded in metadata and listed in the `dot mod` picker). The contract + * records this bit so the playground-app filter doesn't need to fetch + * each metadata JSON to know. + */ + isModdable?: boolean; + /** + * Domain (`