From 1d82fd9eb4a8e726f262e43d6ea69227285df110 Mon Sep 17 00:00:00 2001 From: sacha <23283108+sacha-l@users.noreply.github.com> Date: Wed, 10 Jun 2026 09:45:31 +0200 Subject: [PATCH] Prevent the "already owned" deploy collision: npm run name:new MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The template ships a hardcoded contract package name (@rps/leaderboard). On the shared registry that name is already owned by the original deployer's signer, so the first thing a modder hits on `pg deploy` is: ✖ CDM package "@rps/leaderboard" is already owned by 0x…, but the selected signer maps to 0x…. Update the Cargo.toml package value to a name you own… The error points at renaming, but every *predictable* replacement (@rps-, @-rps, …) tends to be taken too, so people collide repeatedly. The real fix is to stop shipping a guessable name. - scripts/new-contract-name.mjs (`npm run name:new`): generate a high-entropy, unowned name (@rps-<8hex>/leaderboard) and rewrite cdm.json + contracts/leaderboard/Cargo.toml. Pass an explicit name to override. - src/utils.ts: derive the package name from cdm.json instead of hardcoding it in three places, so there is a single source of truth and the rename only has to touch cdm.json + Cargo.toml (no silent frontend resolve failures). - README: document `npm run name:new` as the first step of modding. Contract-agnostic — no change to contract logic or the deploy pipeline. Co-Authored-By: Claude Opus 4.8 --- README.md | 18 +++++++++ package.json | 1 + scripts/new-contract-name.mjs | 72 +++++++++++++++++++++++++++++++++++ src/utils.ts | 17 +++++++-- 4 files changed, 105 insertions(+), 3 deletions(-) create mode 100644 scripts/new-contract-name.mjs diff --git a/README.md b/README.md index ce612ff..8c1c1bb 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,24 @@ npm run dev ``` Open polkadot desktop on localhost. +## Modding this app — claim a unique contract name first + +Before you deploy your own copy, **claim a fresh contract package name**: + +```bash +npm run name:new +``` + +The template ships a hardcoded contract package name (`@rps/leaderboard`). On the +shared registry that name is **already owned by the original deployer's signer**, +so `pg deploy` / `npm run deploy` fails with `already owned by 0x…` and points you +at renaming — which collides again under the next predictable name. `npm run +name:new` generates a high-entropy name (`@rps-<8hex>/leaderboard`) that nobody +owns and rewrites it in `cdm.json` + `contracts/leaderboard/Cargo.toml` (the +frontend derives the name from `cdm.json`, so there's nothing else to touch). Run +it once when you start a mod and the ownership collision can't happen. Pass your +own name if you prefer: `npm run name:new @me/scoreboard`. + > Deploying **your own copy** (own contract, own `.dot` name, published to the > playground)? Follow the step-by-step [DEPLOYMENT.md](./DEPLOYMENT.md). diff --git a/package.json b/package.json index c1bf590..2486d23 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "build:frontend": "tsc -b && vite build", "build:contracts": "cdm build", "deploy": "cdm deploy -n paseo --registry-address 0xf62c2ece29cd8df2e10040ecfa5a894a5c5d9cb0 --assethub-url wss://paseo-asset-hub-next-rpc.polkadot.io --bulletin-url wss://paseo-bulletin-next-rpc.polkadot.io", + "name:new": "node scripts/new-contract-name.mjs", "preview": "vite preview" }, "dependencies": { diff --git a/scripts/new-contract-name.mjs b/scripts/new-contract-name.mjs new file mode 100644 index 0000000..0b88cb1 --- /dev/null +++ b/scripts/new-contract-name.mjs @@ -0,0 +1,72 @@ +#!/usr/bin/env node +// Claim a fresh, unique contract package name so a freshly-modded app never +// collides with a name already owned by another signer — the recurring deploy +// trap where `pg deploy` fails with `already owned by 0x…` (see DEVEX-REPORT.md +// #1 and DEPLOY-LOG.md). The template ships a hardcoded name that someone has +// usually already claimed; this generates a high-entropy org so the name is +// effectively guaranteed unowned, then rewrites the single source of truth +// (cdm.json) and the contract's Cargo.toml. The frontend derives the name from +// cdm.json at runtime (see src/utils.ts: stageCdmJson), so there is nothing else +// to touch — no hand-syncing across files, no silent resolve failures. +// +// Usage: +// node scripts/new-contract-name.mjs # -> @rps-<8hex>/leaderboard +// node scripts/new-contract-name.mjs @me/scoreboard # use an explicit name +// +// or via npm: npm run name:new + +import { readFileSync, writeFileSync } from "node:fs"; +import { randomBytes } from "node:crypto"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; + +const root = join(dirname(fileURLToPath(import.meta.url)), ".."); +const CDM = join(root, "cdm.json"); +const CARGO = join(root, "contracts/leaderboard/Cargo.toml"); + +const cdm = JSON.parse(readFileSync(CDM, "utf8")); +const current = + Object.keys(cdm.contracts ?? {})[0] ?? Object.keys(cdm.dependencies ?? {})[0]; +if (!current) { + console.error("✖ No contract package found in cdm.json (contracts/dependencies empty)."); + process.exit(1); +} + +// Preserve the package suffix (e.g. "leaderboard"); only the org must be unique. +const suffix = current.includes("/") ? current.split("/").slice(1).join("/") : "leaderboard"; +const next = process.argv[2] ?? `@rps-${randomBytes(4).toString("hex")}/${suffix}`; + +if (!/^@[a-z0-9-]+\/[a-z0-9/-]+$/.test(next)) { + console.error(`✖ Invalid package name: ${next}`); + console.error(" Expected @org/name — lowercase letters, digits and dashes only."); + process.exit(1); +} +if (next === current) { + console.error(`✖ New name is identical to the current one (${current}). Nothing to do.`); + process.exit(1); +} + +// Text replacement (not JSON round-trip) so formatting and key order are untouched; +// the name appears only as a literal string, so split/join is exact and safe. +let changed = 0; +for (const file of [CDM, CARGO]) { + const before = readFileSync(file, "utf8"); + const after = before.split(current).join(next); + if (after !== before) { + writeFileSync(file, after); + changed++; + } +} + +console.log(`✓ Contract package renamed:`); +console.log(` ${current}`); +console.log(` → ${next}`); +console.log(` Updated ${changed} file(s): cdm.json, contracts/leaderboard/Cargo.toml.`); +console.log(""); +console.log("Next:"); +console.log(" 1. npm run dev → verify your mod at http://localhost:3000/?mock"); +console.log(" 2. pg build"); +console.log(" 3. pg deploy --contracts --playground --moddable \\"); +console.log(" --domain --signer phone --buildDir dist"); +console.log(""); +console.log("The name is high-entropy and unowned, so it claims cleanly under your signer."); diff --git a/src/utils.ts b/src/utils.ts index 4860256..7d44041 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -305,9 +305,19 @@ function wrapContract(contract: any): any { let _cdmJson: any = null; let _contractInitPromise: Promise | null = null; +// Single source of truth for the leaderboard package name. Derived from cdm.json +// at stage time instead of hardcoded, so a rename (e.g. `npm run name:new`) only +// touches cdm.json — not three hand-synced string literals where a miss surfaces +// as a silent runtime resolve failure. +let _libraryName: string | null = null; + /** Stage cdm.json without opening the Asset Hub chain client yet. */ export function stageCdmJson(cdmJson: any): void { _cdmJson = cdmJson; + _libraryName = + Object.keys(cdmJson?.contracts ?? {})[0] ?? + Object.keys(cdmJson?.dependencies ?? {})[0] ?? + null; } /** @@ -361,11 +371,12 @@ async function ensureContractsReady(): Promise { } // Map the product account BEFORE live registry resolution. `fromLiveClient` - // immediately calls `registry.getAddress("@rps/leaderboard")` as a view, and + // immediately calls `registry.getAddress()` as a view, and // pallet-revive dry-run-fails that call with `Revive::AccountUnmapped` when the // query origin isn't mapped — surfacing as ContractLiveAddressResolutionError. // Build a plain runtime (no registry query) to perform the mapping first. // (ChainSubmit permission already granted at the top of this init.) + if (!_libraryName) throw new Error("[CDM] No contract package found in cdm.json"); const initRuntime = createContractRuntimeFromClient(_polkadotClient, paseo_asset_hub); await mapAccountWithRuntime(initRuntime, _state.account); @@ -377,10 +388,10 @@ async function ensureContractsReady(): Promise { defaultOrigin: _state.account.address as never, defaultSigner: _state.account.signer, registryOrigin: _state.account.address as never, - libraries: ["@rps/leaderboard"], + libraries: [_libraryName], }, ); - _contract = wrapContract(_contractManager.getContract("@rps/leaderboard")); + _contract = wrapContract(_contractManager.getContract(_libraryName)); console.log("[CDM] Contract manager ready (live registry resolution)"); })(); return _contractInitPromise;