Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
72 changes: 72 additions & 0 deletions scripts/new-contract-name.mjs
Original file line number Diff line number Diff line change
@@ -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 <your-domain> --signer phone --buildDir dist");
console.log("");
console.log("The name is high-entropy and unowned, so it claims cleanly under your signer.");
17 changes: 14 additions & 3 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,9 +305,19 @@ function wrapContract(contract: any): any {
let _cdmJson: any = null;
let _contractInitPromise: Promise<void> | 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;
}

/**
Expand Down Expand Up @@ -361,11 +371,12 @@ async function ensureContractsReady(): Promise<void> {
}

// Map the product account BEFORE live registry resolution. `fromLiveClient`
// immediately calls `registry.getAddress("@rps/leaderboard")` as a view, and
// immediately calls `registry.getAddress(<library>)` 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);

Expand All @@ -377,10 +388,10 @@ async function ensureContractsReady(): Promise<void> {
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;
Expand Down