Skip to content
Draft
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,6 @@ broadcast/*
.aider*
.codeium/
.gemini/

# Generated CDM registration assets (deployment-specific; see scripts/cdm/)
.generated/
43 changes: 43 additions & 0 deletions DEPLOYMENT_CHECKLIST.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,49 @@ Confirm these keys are present:

Done. ✅

## Step 9 — (Optional) Publish the deployment to the CDM registry

Registering the deployment in the [CDM contract registry](https://github.com/paritytech/contract-dependency-manager)
lets consumers resolve the current DotNS addresses with `cdm install @dotns/<package>`
instead of chasing address changes across redeploys.

```bash
# Generate CDM assets from the manifest you just deployed (needs `forge build` output):
bun run cdm:generate -- \
--deployment deployments/<folder>/<CHAINID>.json \
--network <network-name>

# Preview what would be published (no transactions):
bun run cdm:register -- --metadata .generated/dotns-cdm/metadata.json \
--name paseo --dry-run

# Publish (the signer needs funds on Asset Hub and Bulletin storage authorization;
# CDM_SURI env var also works, preferred in CI):
bun run cdm:register -- --metadata .generated/dotns-cdm/metadata.json \
--name paseo --suri "<mnemonic>"
```

For networks without a CDM preset, pass explicit endpoints:

```bash
bun run cdm:register -- --metadata .generated/dotns-cdm/metadata.json \
--name custom \
--assethub-url wss://... --bulletin-url wss://... \
--registry-address 0x... \
--suri "$CDM_SURI"
```

Notes:

- Re-running is safe: contracts whose registered address and metadata already
match are skipped, so a partially-failed run can simply be re-run.
- The first registration of each `@dotns/*` package claims its name for the
signing account; use the same account for subsequent redeploys.
- See `scripts/deploy/cdm/` for how the assets are generated (static descriptors +
readmes in the repo, addresses from the manifest, ABIs from `out/**`).

Done. ✅

## Troubleshooting — the four things that actually go wrong

- **`PRIVATE_KEY is required`** — the keystore account doesn't exist yet. Make
Expand Down
545 changes: 545 additions & 0 deletions bun.lock

Large diffs are not rendered by default.

11 changes: 9 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
"deploy:testnet": "./scripts/deploy/run.sh '--timeout 1000'",
"deploy:factory": "./scripts/deploy/factory.sh",
"deploy:all": "./scripts/deploy/deployall.sh",
"cdm:generate": "bun scripts/deploy/cdm/generate.ts",
"cdm:register": "bun scripts/deploy/cdm/register.ts",
"postinstall": "./setup.bash",
"reset": "forge clean"
},
Expand Down Expand Up @@ -43,6 +45,11 @@
"node": "24.6.0"
},
"dependencies": {
"@openzeppelin/upgrades-core": "1.44.2"
"@openzeppelin/upgrades-core": "1.44.2",
"@parity/cdm-builder": "^5.0.0",
"@parity/cdm-env": "^2.3.0",
"@parity/product-sdk-cloud-storage": "^0.5.4",
"@parity/product-sdk-contracts": "^0.9.0",
"@parity/product-sdk-tx": "^0.2.8"
}
}
}
137 changes: 137 additions & 0 deletions scripts/deploy/cdm/descriptors.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
{
"repository": "https://github.com/paritytech/dotns",
"homepage": "https://github.com/paritytech/dotns#readme",
"authors": [
"Parity Technologies"
],
"contracts": [
{
"name": "DotnsProtocolRegistry",
"cdmPackage": "@dotns/protocol-registry",
"description": "dotNS protocol-level address registry used by contracts to resolve sibling protocol components at runtime.",
"sourcePath": "contracts/registry/DotnsProtocolRegistry.sol",
"artifactContract": "DotnsProtocolRegistry",
"readme": "DotnsProtocolRegistry.md"
},
{
"name": "Multicall3",
"cdmPackage": "@dotns/multicall3",
"description": "Generic Multicall3 helper for batching dotNS and other Asset Hub contract reads.",
"sourcePath": "contracts/utils/Multicall3.sol",
"artifactContract": "Multicall3",
"readme": "Multicall3.md"
},
{
"name": "DotnsRegistrar",
"cdmPackage": "@dotns/registrar",
"description": "ERC721 token of record for .dot names, with controller-gated minting and PopRules-priced, fee-gated transfers.",
"sourcePath": "contracts/registrars/DotnsRegistrar.sol",
"artifactContract": "DotnsRegistrar",
"readme": "DotnsRegistrar.md"
},
{
"name": "DotnsRegistry",
"cdmPackage": "@dotns/registry",
"description": "Forward dotNS registry for node ownership, resolver selection, and subname records.",
"sourcePath": "contracts/registry/DotnsRegistry.sol",
"artifactContract": "DotnsRegistry",
"readme": "DotnsRegistry.md"
},
{
"name": "DotnsRegistrarController",
"cdmPackage": "@dotns/registrar-controller",
"description": "Commit-reveal public registration controller for dotNS names.",
"sourcePath": "contracts/registrars/DotnsRegistrarController.sol",
"artifactContract": "DotnsRegistrarController",
"readme": "DotnsRegistrarController.md"
},
{
"name": "DotnsPopController",
"cdmPackage": "@dotns/pop-controller",
"description": "Gateway-only controller issuing lite- and full-person PoP usernames, with per-label base-name reservation queues.",
"sourcePath": "contracts/registrars/DotnsPopController.sol",
"artifactContract": "DotnsPopController",
"readme": "DotnsPopController.md"
},
{
"name": "RootGatewayDispatcher",
"cdmPackage": "@dotns/root-gateway-dispatcher",
"description": "Root-authority dispatcher that forwards verified gateway calls into the PoP controller.",
"sourcePath": "contracts/registrars/RootGatewayDispatcher.sol",
"artifactContract": "RootGatewayDispatcher",
"readme": "RootGatewayDispatcher.md"
},
{
"name": "PopRules",
"cdmPackage": "@dotns/pop-rules",
"description": "Proof-of-Personhood policy and pricing rules for dotNS registration and transfer flows.",
"sourcePath": "contracts/pop/PopRules.sol",
"artifactContract": "PopRules",
"readme": "PopRules.md"
},
{
"name": "DotnsResolver",
"cdmPackage": "@dotns/resolver",
"description": "Forward resolver mapping each dotNS node to a single address record, writable by the node owner.",
"sourcePath": "contracts/resolvers/DotnsResolver.sol",
"artifactContract": "DotnsResolver",
"readme": "DotnsResolver.md"
},
{
"name": "DotnsReverseResolver",
"cdmPackage": "@dotns/reverse-resolver",
"description": "Reverse resolver that maps owner accounts back to dotNS names.",
"sourcePath": "contracts/resolvers/DotnsReverseResolver.sol",
"artifactContract": "DotnsReverseResolver",
"readme": "DotnsReverseResolver.md"
},
{
"name": "DotnsContentResolver",
"cdmPackage": "@dotns/content-resolver",
"description": "Contenthash and text-record resolver for dotNS nodes, with operator-delegated record management.",
"sourcePath": "contracts/resolvers/DotnsContentResolver.sol",
"artifactContract": "DotnsContentResolver",
"readme": "DotnsContentResolver.md"
},
{
"name": "DotnsPopResolver",
"cdmPackage": "@dotns/pop-resolver",
"description": "Resolver for PoP-issued records: per-node chat keys and bidirectional lite/full username links.",
"sourcePath": "contracts/resolvers/DotnsPopResolver.sol",
"artifactContract": "DotnsPopResolver",
"readme": "DotnsPopResolver.md"
},
{
"name": "DotnsNameEscrow",
"cdmPackage": "@dotns/name-escrow",
"description": "Escrow contract for dotNS release positions, transfer friction fees, and overpayment accounting.",
"sourcePath": "contracts/escrow/DotnsNameEscrow.sol",
"artifactContract": "DotnsNameEscrow",
"readme": "DotnsNameEscrow.md"
},
{
"name": "StoreFactory",
"cdmPackage": "@dotns/store-factory",
"description": "Factory deploying each user's permanent LabelStore and UserStore as beacon proxies behind two upgradeable beacons.",
"sourcePath": "contracts/store/StoreFactory.sol",
"artifactContract": "StoreFactory",
"readme": "StoreFactory.md"
},
{
"name": "LabelStoreBeacon",
"cdmPackage": "@dotns/label-store-beacon",
"description": "Upgradeable beacon that points StoreFactory-created label stores at the current LabelStore implementation.",
"sourcePath": "contracts/store/LabelStore.sol",
"artifactContract": "UpgradeableBeacon",
"readme": "LabelStoreBeacon.md"
},
{
"name": "UserStoreBeacon",
"cdmPackage": "@dotns/user-store-beacon",
"description": "Upgradeable beacon that points StoreFactory-created user stores at the current UserStore implementation.",
"sourcePath": "contracts/store/UserStore.sol",
"artifactContract": "UpgradeableBeacon",
"readme": "UserStoreBeacon.md"
}
]
}
171 changes: 171 additions & 0 deletions scripts/deploy/cdm/generate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
#!/usr/bin/env bun
// Generate CDM registration assets from a DotNS deployment.
//
// Reads three inputs and produces one self-contained asset directory that
// `scripts/cdm/register.ts` can publish to a CDM contract registry:
//
// 1. A deployment manifest (`deployments/<network-folder>/<chain-id>.json`),
// the flat {"ContractName": "0xaddress"} map written by the deploy
// pipeline — this is where addresses come from.
// 2. Foundry build artifacts (`out/<File>.sol/<Contract>.json`) — this is
// where ABIs come from. Run `forge build` first.
// 3. The static descriptor map (`scripts/deploy/cdm/descriptors.json`) + readmes
// (`scripts/cdm/readmes/*.md`) — checked-in, deployment-independent
// metadata: CDM package names, descriptions, docs.
//
// Nothing in the output is hand-maintained: addresses and ABIs always come
// from the actual deployment and build, so the generated assets cannot drift
// from what is on chain (see issue: "Generate CDM registration metadata from
// DotNS deployments").
//
// Local usage:
// forge build
// bun run cdm:generate -- \
// --deployment deployments/paseo-assethub/420420417.json \
// --network paseo-assethub-previewnet
//
// Output (git-ignored):
// .generated/dotns-cdm/
// metadata.json manifest consumed by register.ts
// abis/*.abi.json extracted from Foundry artifacts
// *.md copied readmes

import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { basename, dirname, join, resolve } from "node:path";
import { parseArgs } from "node:util";

const REPO_ROOT = resolve(dirname(Bun.main), "../../..");

const { values: flags } = parseArgs({
options: {
deployment: { type: "string" },
network: { type: "string" },
out: { type: "string", default: ".generated/dotns-cdm" },
"cdm-registry": { type: "string" },
help: { type: "boolean", short: "h" },
},
});

if (flags.help || !flags.deployment || !flags.network) {
console.log(
`Usage: bun run cdm:generate -- --deployment deployments/<folder>/<chain-id>.json --network <name> [--out .generated/dotns-cdm] [--cdm-registry <address>]`,
);
process.exit(flags.help ? 0 : 1);
}

const readJson = (path: string) => JSON.parse(readFileSync(path, "utf8"));

interface Descriptor {
name: string;
cdmPackage: string;
description: string;
sourcePath: string;
artifactContract: string;
readme: string;
}

const descriptorsPath = join(REPO_ROOT, "scripts/deploy/cdm/descriptors.json");
const descriptorDoc = readJson(descriptorsPath) as {
repository: string;
homepage: string;
authors: string[];
contracts: Descriptor[];
};

const deploymentPath = resolve(flags.deployment);
if (!existsSync(deploymentPath)) {
console.error(`Deployment manifest not found: ${deploymentPath}`);
process.exit(1);
}
const deployment = readJson(deploymentPath) as Record<string, string>;

// The deploy pipeline's serialization sentinel; never a contract.
delete deployment._seed;

// Every descriptor must have a deployed address — a manifest missing any
// published contract is a partial deploy and must not be registered.
const missing = descriptorDoc.contracts
.map((d) => d.name)
.filter((name) => !/^0x[0-9a-fA-F]{40}$/.test(deployment[name] ?? ""));
if (missing.length > 0) {
console.error(
`Deployment manifest ${flags.deployment} is missing required contracts:\n ${missing.join("\n ")}`,
);
process.exit(1);
}

// The reverse — deployed contracts we have no descriptor for — is fine
// (Create3Factory is deployment infrastructure, not a CDM package), but say
// so, in case a new contract was added without a descriptor.
const undescribed = Object.keys(deployment).filter(
(name) => !descriptorDoc.contracts.some((d) => d.name === name),
);
if (undescribed.length > 0) {
console.log(`Skipping manifest entries with no CDM descriptor: ${undescribed.join(", ")}`);
}

const outDir = resolve(flags.out);
mkdirSync(join(outDir, "abis"), { recursive: true });

const contracts = descriptorDoc.contracts.map((descriptor) => {
// Foundry keys artifacts by source file, and every artifact we publish
// lives in a file named after its contract (<Contract>.sol/<Contract>.json)
// — including UpgradeableBeacon, which comes from OpenZeppelin.
const artifactPath = join(
REPO_ROOT,
"out",
`${descriptor.artifactContract}.sol`,
`${descriptor.artifactContract}.json`,
);
if (!existsSync(artifactPath)) {
console.error(
`Foundry artifact not found: ${artifactPath}\n` +
`(descriptor ${descriptor.name} → artifact contract ${descriptor.artifactContract}; run \`forge build\` first)`,
);
process.exit(1);
}
const abi = readJson(artifactPath).abi;
if (!Array.isArray(abi) || abi.length === 0) {
console.error(`Artifact has no ABI: ${artifactPath}`);
process.exit(1);
}

const readmeSource = join(REPO_ROOT, "scripts/deploy/cdm/readmes", descriptor.readme);
if (!existsSync(readmeSource)) {
console.error(`Readme not found for ${descriptor.name}: ${readmeSource}`);
process.exit(1);
}

const abiPath = `abis/${descriptor.name}.abi.json`;
const readmePath = basename(descriptor.readme);
writeFileSync(join(outDir, abiPath), `${JSON.stringify(abi, null, 2)}\n`);
copyFileSync(readmeSource, join(outDir, readmePath));

return {
name: descriptor.name,
cdmPackage: descriptor.cdmPackage,
address: deployment[descriptor.name],
description: descriptor.description,
abiPath,
readmePath,
sourcePath: descriptor.sourcePath,
contractName: descriptor.artifactContract,
};
});

const metadata = {
version: 1,
// Also used by register.ts as the published_at timestamp, so re-registering
// the same generated assets produces byte-identical metadata (and thus the
// same content CIDs), which is what makes registration idempotent.
generatedAt: new Date().toISOString(),
network: flags.network,
...(flags["cdm-registry"] ? { registry: flags["cdm-registry"] } : {}),
repository: descriptorDoc.repository,
homepage: descriptorDoc.homepage,
authors: descriptorDoc.authors,
contracts,
};

writeFileSync(join(outDir, "metadata.json"), `${JSON.stringify(metadata, null, 2)}\n`);
console.log(`Generated CDM assets for ${contracts.length} contracts at ${outDir}`);
Loading
Loading