Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/strip-contract-deploy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"playground-cli": patch
---

Move contract deployment out of `dot deploy` and add CDM-backed `dot contract deploy/install` commands. `dot contract deploy` now calls CDM's deploy pipeline with dot's signer and Bulletin allowance signer, uses CDM's current registry defaults from `@dotdm/env`, renders a CDM-style Ink progress table using dot's shared TUI primitives, and `dot contract install` delegates to CDM's installer.
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ These aren't self-evident from reading the code and have bitten us before. Treat
### Dependency pins / lockfile

- **Import from `@parity/product-sdk-*`, never `@polkadot-apps/*`.** The CLI runtime is fully on product-sdk. `@polkadot-apps/*` is gone from the lockfile and CI's `Format` job runs `grep -rnE "['\"]@polkadot-apps/" src/ e2e/ scripts/ tools/` as a guard. Product-sdk uses caret ranges (`^0.x.y`); on a 0.x line `^` only widens patches, so a true breaking change still needs an explicit `package.json` bump.
- **`@dotdm/contracts` is on `^2.0.3`.** The 2.0 line ships `resolveTargetRegistryAddress` and re-exports `REGISTRY_ADDRESS` from `@dotdm/utils`, both used by `src/config.ts::CDM_REGISTRY_ADDRESS`. The legacy `1.1.1` stable still depends on `@polkadot-apps/*` + PAPI 1.x — do NOT downgrade.
- **`@dotdm/contracts` tracks the `^3.x` line.** The legacy `1.1.1` stable still depends on `@polkadot-apps/*` + PAPI 1.x — do NOT downgrade.
- **`@novasamatech/*` packages are forced to `0.7.9-4` via `pnpm.overrides`.** They're transitive (via `@parity/product-sdk-terminal`'s `^0.7.7` ranges) and pnpm won't bump transitives across patches. The override aligns the tree on the latest published Novasama line including RFC-0010 `requestResourceAllocation`. Drop the override once product-sdk-terminal bumps its caret natively.
- **`@polkadot-api/json-rpc-provider: ^0.2.0` override is load-bearing.** Removing it splits the lockfile across three versions of `json-rpc-provider` (`0.0.1`/`0.0.4`/`0.2.0`) — different PAPI 2.x transitive consumers ask for different versions. Forcing everyone onto `0.2.0` avoids subtle wire-shape divergence and reduces bundle/process memory.
- **`@parity/dotns-cli@0.6.1` ships a broken publish manifest** declaring `"@polkadot-api/descriptors": "file:.papi/descriptors"` — a workspace path missing from the tarball. pnpm refuses; we redirect that sub-dep to `stubs/papi-descriptors-stub/` (an empty `{}` export). dotns-cli's `dist/cli.js` is a fully-bundled Bun build, so the stub is functionally correct. Remove the override + stub when `@parity/dotns-cli` republishes a clean manifest.
Expand Down
16 changes: 11 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,15 +55,13 @@ Flags:
- `--domain <name>` — DotNS label (with or without the `.dot` suffix). Interactive prompt if omitted.
- `--buildDir <path>` — directory holding the built artifacts (default `dist/`). Interactive prompt if omitted.
- `--no-build` — skip the frontend build step and deploy whatever is already in `--buildDir`.
- `--contracts` — also compile and deploy the foundry / hardhat / cdm contract project at the project root (interactive prompt if a contract project is detected and the flag is omitted; skipped automatically when no project is detected).
- `--no-contract-build` — skip the contract compile step (`forge build --resolc`, `cargo-contract build`, `npx hardhat compile`) and deploy pre-built artifacts. Requires `--contracts` and headless mode (i.e. all of `--signer`, `--domain`, `--buildDir`, `--playground`).
- `--playground` — publish to the playground registry so the app appears under "my apps". Interactive prompt (default: no) if omitted.
- `--private` — publish to the playground with private (owner-only) visibility. Requires `--playground`. Not interactively prompted; pass the flag to opt in.
- `--moddable` / `--no-moddable` — publish the source repo URL alongside the deploy so others can `dot mod` it. Requires `--playground`. Interactive prompt (default: no) if omitted. The CLI reads your existing `origin` and records its URL in the Bulletin metadata; it never creates a repo or pushes for you. The deploy fails with an actionable message if `origin` is unset, points to a private repo, or points to anything other than GitHub (since `dot mod` only fetches from `codeload.github.com`). Set up the repo yourself before re-running: create a public repo on GitHub, then `git remote add origin https://github.com/<user>/<repo>` followed by `git push -u origin main`. (If you happen to have `gh` installed, `gh repo create my-app --public --source=. --push` does both in one shot — `dot` does not require `gh`.)
- `--suri <suri>` — override signer with a dev secret URI (e.g. `//Alice`). Useful for CI.
- `--env <env>` — target environment. Defaults to `paseo-next-v2` (the only one fully wired today). Accepts the bulletin-deploy env IDs (`preview`, `paseo-next`, `paseo-review`, `paseo-next-v2`, `polkadot`, `kusama`) plus the legacy `testnet`/`mainnet` aliases — `testnet` maps to `paseo-next-v2`, `mainnet` to `polkadot`. Any env other than `paseo-next-v2` throws "not supported" until its entry is wired up in `src/config.ts::CONFIGS`.

Passing all four of `--signer`, `--domain`, `--buildDir`, and `--playground` runs in fully non-interactive mode. Any absent flag is filled in by the TUI prompt. `--moddable`, `--private`, and `--contracts` are independently optional in both modes — their absence means a non-moddable, public, frontend-only deploy.
Passing all four of `--signer`, `--domain`, `--buildDir`, and `--playground` runs in fully non-interactive mode. Any absent flag is filled in by the TUI prompt. `--moddable` and `--private` are independently optional in both modes — their absence means a non-moddable, public deploy.

**Requirement**: the `ipfs` CLI (Kubo) must be on `PATH`. `dot init` installs it; if you skipped init you can install it manually (`brew install ipfs` or follow [docs.ipfs.tech/install](https://docs.ipfs.tech/install/)). This is a temporary requirement while `bulletin-deploy`'s pure-JS merkleizer has a bug that makes the browser fallback unusable.

Expand All @@ -75,10 +73,18 @@ For fully non-interactive (CI) runs, combine `--signer`, `--domain`, `--buildDir

- `--suri //Alice` — required with `--signer dev` so the dev signer has a known keypair (works with any dev name or full BIP-39 mnemonic).
- `--no-build` — reuse pre-built frontend assets in `--buildDir`.
- `--contracts` + `--no-contract-build` — reuse pre-built contract artefacts in `out/` / `target/<crate>.release.polkavm` / `artifacts/contracts/` (skips `forge build --resolc`, `cargo-contract build`, or `npx hardhat compile`).
- `--no-moddable` — explicitly skip source publishing even if `--moddable` would otherwise apply.
- `--private` — publish to the playground with owner-only visibility.

### `dot contract`

CDM-backed workflows for contracts:

- `dot contract deploy` builds, deploys, and registers CDM contracts with dot's logged-in signer by default. Pass `--suri //Alice` for local/dev signing.
- `dot contract deploy --features <features>` forwards Cargo feature flags into CDM's build pipeline.
- `dot contract deploy --registry-address <address>` targets a specific CDM registry.
- `dot contract install [libraries...]` runs `cdm install [libraries...]`; CDM still owns dependency installation and post-install hooks.

### `dot mod`

Pull a moddable playground app's source into a fresh local project so you can customise and re-deploy it. The interactive picker only shows apps that opted into moddable at deploy time; non-moddable apps surface a clear "this app is not moddable" error if you target them by domain.
Expand Down Expand Up @@ -216,7 +222,7 @@ The first two are also enforced in CI; running them locally catches the failure
## Dependency Notes

- `@parity/product-sdk-*` packages use caret ranges (`^0.x.y`) so upstream patch and minor releases auto-resolve on a fresh `pnpm install`. With pre-1.0 versions, `^` only widens patches within the current 0.x line — a 0.x → 0.(x+1) bump still requires an intentional `package.json` change. CI's `Format` job runs a grep guard that fails the build on any direct `@polkadot-apps/*` import in `src/`, `e2e/`, `scripts/`, or `tools/`.
- `@dotdm/contracts` is on the `^2.0.x` caret. The 2.0 line is the first to consume `@parity/product-sdk-*` directly; the legacy `1.1.1` stable still pulls `@polkadot-apps/*` + `polkadot-api@1.x` and must NOT be downgraded to. Patch bumps within 2.x are safe.
- `@dotdm/contracts` is on the `^3.x` caret. The legacy `1.1.1` stable still pulls `@polkadot-apps/*` + `polkadot-api@1.x` and must NOT be downgraded to.
- `@novasamatech/*` packages are forced to `0.7.9-4` via `pnpm.overrides`. They come in transitively from `@parity/product-sdk-terminal` whose `^0.7.7` caret doesn't auto-widen across patches in lockfile updates; the override aligns the tree on the latest published Novasama line (including RFC-0010 `requestResourceAllocation` on `UserSession`). Drop the override once product-sdk-terminal widens its own caret.
- `polkadot-api` is on `^2.1.x` and `@polkadot-api/sdk-ink` on `^0.7.0`. The lockfile contains a stale `polkadot-api@1.x` only because `@parity/dotns-cli`'s declared dep references it; that CLI ships as a single bundled `dist/cli.js` with all deps inlined, so the 1.x decl is never resolved at runtime. Effectively the runtime is PAPI 2.x-only.
- `bulletin-deploy` is pinned to an explicit version — not `latest`. Currently `0.7.24`. Previously `latest` pointed at 0.6.8 which had a WebSocket heartbeat bug (40s default < 60s chunk timeout) that tore chunk uploads down as `WS halt (3)`; keeping the pin explicit avoids ever sliding back onto that. When bumping, check the release notes for any changes to `deploy()` / `DotNS` APIs we rely on (`jsMerkle`, `signer`, `signerAddress`, `mnemonic`, `rpc`, `attributes`).
Expand Down
215 changes: 0 additions & 215 deletions e2e/cli/deploy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,6 @@
* All headless deploys require: --signer, --domain, --buildDir, --playground
* to trigger the non-interactive path (see isFullySpecified() in deploy/index.ts).
*
* Developer-requested priorities:
* - Projects with multiple contracts (multi-contract fixture)
* - EVM (Foundry/Hardhat) vs PVM (Rust/CDM) backends
* - The --contracts flag
*/

import { describe, test, expect } from "vitest";
Expand All @@ -45,59 +41,12 @@ function extractMetadataCid(stdout: string): string | null {
}

const frontendOnly = fixturePath("frontend-only");
const foundry = fixturePath("foundry");
const hardhat = fixturePath("hardhat");
const rustCdm = fixturePath("rust-cdm");
const multiContract = fixturePath("multi-contract");

/** buildDir must be absolute — it's resolved relative to cwd, not --dir */
function absBuildDir(fixture: string, dir = "dist"): string {
return resolve(fixture, dir);
}

/**
* Shared helper for contract-deploy end-to-end tests.
*
* `--no-contract-build` skips the toolchain subprocess (forge / npx hardhat
* compile / cargo-contract) so the CI runner doesn't need the EVM/Rust
* toolchain installed. Each fixture ships pre-built bytecode in its out/ or
* artifacts/ directory.
*/
interface ContractDeployTestConfig {
/** describe-block discriminator: "foundry", "hardhat", "multi" */
name: string;
/** E2E_DOMAINS.<name> */
domain: string;
/** fixturePath() result */
fixture: string;
}

function runContractDeployTest(cfg: ContractDeployTestConfig): void {
describe(`dot deploy — ${cfg.name} (requires Paseo + IPFS)`, () => {
test(`${cfg.name} deploy completes end-to-end`, { timeout: 450_000 }, async () => {
const result = await dot([
"deploy",
"--signer", "dev",
"--domain", cfg.domain,
"--buildDir", absBuildDir(cfg.fixture),
"--contracts",
"--no-contract-build",
"--playground",
"--private",
"--suri", SIGNER.suri,
"--dir", cfg.fixture,
], { timeout: 400_000 });

expect(
result.exitCode,
`${cfg.name} deploy failed: ${result.stdout}\n${result.stderr}`,
).toBe(0);
expect(result.stdout).toContain("Deploy complete");
expect(result.stdout).toContain(cfg.domain);
});
});
}

/**
* Assertion notes for the preflight tests below:
* - "Checking availability" is printed by `src/commands/deploy/index.ts` ONLY
Expand Down Expand Up @@ -134,113 +83,6 @@ describe("dot deploy — preflight and validation", () => {
expect(output).toContain("--env paseo-next-v2");
});

test("detects foundry contracts type in project", async () => {
const result = await dot([
"deploy",
"--signer", "dev",
"--domain", E2E_DOMAINS.preflight,
"--buildDir", absBuildDir(foundry),
"--no-build",
"--contracts",
"--playground",
"--private",
"--suri", SIGNER.suri,
"--dir", foundry,
]);
const output = result.stdout + result.stderr;
// foundry.toml present → should not complain about missing contract project
expect(output).not.toContain("no foundry/hardhat/cdm project was detected");
// Real checkpoint: only printed after preflight succeeds.
expect(
output,
`expected to reach availability check\n${output}`,
).toContain("Checking availability");
});

test("detects hardhat contracts type in project", async () => {
const result = await dot([
"deploy",
"--signer", "dev",
"--domain", E2E_DOMAINS.preflight,
"--buildDir", absBuildDir(hardhat),
"--no-build",
"--contracts",
"--playground",
"--private",
"--suri", SIGNER.suri,
"--dir", hardhat,
]);
const output = result.stdout + result.stderr;
expect(output).not.toContain("no foundry/hardhat/cdm project was detected");
expect(
output,
`expected to reach availability check\n${output}`,
).toContain("Checking availability");
});

test("detects CDM/Rust contracts type in project", async () => {
const result = await dot([
"deploy",
"--signer", "dev",
"--domain", E2E_DOMAINS.preflight,
"--buildDir", absBuildDir(rustCdm),
"--no-build",
"--contracts",
"--playground",
"--private",
"--suri", SIGNER.suri,
"--dir", rustCdm,
]);
const output = result.stdout + result.stderr;
expect(output).not.toContain("no foundry/hardhat/cdm project was detected");
expect(
output,
`expected to reach availability check\n${output}`,
).toContain("Checking availability");
});

test("detects multiple contracts in multi-contract project", async () => {
const result = await dot([
"deploy",
"--signer", "dev",
"--domain", E2E_DOMAINS.preflight,
"--buildDir", absBuildDir(multiContract),
"--no-build",
"--contracts",
"--playground",
"--private",
"--suri", SIGNER.suri,
"--dir", multiContract,
]);
const output = result.stdout + result.stderr;
expect(output).not.toContain("no foundry/hardhat/cdm project was detected");
expect(
output,
`expected to reach availability check\n${output}`,
).toContain("Checking availability");
});

test("--contracts reports error when no contract project detected", async () => {
const result = await dot([
"deploy",
"--signer", "dev",
"--domain", E2E_DOMAINS.preflight,
"--buildDir", absBuildDir(frontendOnly),
"--no-build",
"--contracts",
"--playground",
"--private",
"--suri", SIGNER.suri,
"--dir", frontendOnly,
], { timeout: 400_000 });
const output = result.stdout + result.stderr;
expect(
result.exitCode,
`expected non-zero exit when --contracts has no project\n${output}`,
).not.toBe(0);
expect(output).toContain("no foundry/hardhat/cdm project was detected");
});

test("domain availability check runs before build/upload", { timeout: 300_000 }, async () => {
const domain = E2E_DOMAINS.preflight;
const result = await dot([
Expand Down Expand Up @@ -409,60 +251,3 @@ describe("dot deploy --playground — full pipeline (requires Paseo + IPFS)", ()
expect(output.toLowerCase()).toMatch(/revert|taken|registered|owned|unavailable|already/);
});
});

// Contract-deploy tests — parametrized via runContractDeployTest
runContractDeployTest({ name: "foundry", domain: E2E_DOMAINS.foundry, fixture: foundry });
runContractDeployTest({ name: "hardhat", domain: E2E_DOMAINS.hardhat, fixture: hardhat });
// Multi-contract foundry project — exercises the contracts-batch publish path
// (TokenA.sol + TokenB.sol deployed in a single --contracts run).
runContractDeployTest({ name: "multi", domain: E2E_DOMAINS.multi, fixture: multiContract });

// Rejection test — does NOT require Paseo or IPFS; exits before any chain mutation.
describe("dot deploy — rejects --no-contract-build with no artefacts", () => {
test("foundry project with --no-contract-build but no out/ → clear error", { timeout: 120_000 }, async () => {
const constructorArgs = fixturePath("constructor-args");
const result = await dot([
"deploy",
"--signer", "dev",
"--domain", E2E_DOMAINS.preflight,
"--buildDir", absBuildDir(constructorArgs),
"--contracts",
"--no-contract-build",
"--playground",
"--private",
"--suri", SIGNER.suri,
"--dir", constructorArgs,
]);
const output = result.stdout + result.stderr;
expect(result.exitCode).not.toBe(0);
expect(output).toMatch(/no pre-built contract artifacts found/i);
expect(output).toMatch(/--no-contract-build/);
});
});

// CDM follows the same CI shape as foundry/hardhat: deploy pre-built artifacts
// committed with the fixture, without requiring the Rust/PVM toolchain on CI.
describe("dot deploy — cdm (requires Paseo + IPFS)", () => {
test("CDM deploy completes end-to-end", { timeout: 450_000 }, async () => {
const domain = E2E_DOMAINS.cdm;
const result = await dot([
"deploy",
"--signer", "dev",
"--domain", domain,
"--buildDir", absBuildDir(rustCdm),
"--contracts",
"--no-contract-build",
"--playground",
"--private",
"--suri", SIGNER.suri,
"--dir", rustCdm,
], { timeout: 400_000 });

expect(
result.exitCode,
`CDM deploy failed: ${result.stdout}\n${result.stderr}`,
).toBe(0);
expect(result.stdout).toContain("Deploy complete");
expect(result.stdout).toContain(domain);
});
});
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
"test:e2e:nightly": "tools/e2e-local.sh nightly"
},
"dependencies": {
"@dotdm/contracts": "^2.0.3",
"@dotdm/contracts": "^3.0.0",
"@dotdm/env": "^2.0.0",
"@parity/dotns-cli": "0.6.1",
"@parity/product-sdk-address": "^0.1.1",
"@parity/product-sdk-bulletin": "^0.4.2",
Expand Down
Loading
Loading