diff --git a/.github/workflows/cli-dev-release.yml b/.github/workflows/cli-dev-release.yml new file mode 100644 index 0000000..f391afa --- /dev/null +++ b/.github/workflows/cli-dev-release.yml @@ -0,0 +1,54 @@ +name: CLI Dev Release + +on: + push: + branches: [ch/cli] + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + with: + bun-version: "1.2.x" + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 + with: + node-version: "22" + + - run: pnpm install + - run: pnpm build + + # Delete previous dev release so we can recreate it + - name: Delete previous dev release + run: gh release delete cli-dev --cleanup-tag -y 2>/dev/null || true + env: + GH_TOKEN: ${{ github.token }} + + - name: Compile CLI binaries + run: | + bun build --compile --target=bun-linux-x64 ./apps/cli/src/index.ts --outfile dot-linux-x64 + bun build --compile --target=bun-linux-arm64 ./apps/cli/src/index.ts --outfile dot-linux-arm64 + bun build --compile --target=bun-darwin-x64 ./apps/cli/src/index.ts --outfile dot-darwin-x64 + bun build --compile --target=bun-darwin-arm64 ./apps/cli/src/index.ts --outfile dot-darwin-arm64 + + - name: Create dev release + uses: softprops/action-gh-release@v2 + with: + tag_name: cli-dev + name: CLI Dev Build + prerelease: true + make_latest: false + files: | + dot-linux-x64 + dot-linux-arm64 + dot-darwin-x64 + dot-darwin-arm64 + body: | + Dev build from `ch/cli` branch. + + Install: `DOT_TAG=cli-dev curl -fsSL https://raw.githubusercontent.com/paritytech/polkadot-apps/ch/cli/install.sh | bash` diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 93d8137..39197f0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -39,6 +39,10 @@ jobs: echo "found=false" >> $GITHUB_OUTPUT fi + - if: steps.bot.outputs.skip != 'true' && steps.changesets.outputs.found == 'true' + uses: oven-sh/setup-bun@v2 + with: + bun-version: "1.2.x" - if: steps.bot.outputs.skip != 'true' && steps.changesets.outputs.found == 'true' uses: pnpm/action-setup@v4 - if: steps.bot.outputs.skip != 'true' && steps.changesets.outputs.found == 'true' @@ -124,3 +128,34 @@ jobs: inputs: '${{ format(''{{ "repo": "{0}", "run_id": "{1}" }}'', github.repository, github.run_id) }}' env: GITHUB_TOKEN: ${{ secrets.NPM_PUBLISH_AUTOMATION_TOKEN }} + + # Compile CLI binaries for all platforms + - name: Compile CLI binaries + if: steps.bot.outputs.skip != 'true' && steps.changesets.outputs.found == 'true' + run: | + bun build --compile --target=bun-linux-x64 ./apps/cli/src/index.ts --outfile dot-linux-x64 + bun build --compile --target=bun-linux-arm64 ./apps/cli/src/index.ts --outfile dot-linux-arm64 + bun build --compile --target=bun-darwin-x64 ./apps/cli/src/index.ts --outfile dot-darwin-x64 + bun build --compile --target=bun-darwin-arm64 ./apps/cli/src/index.ts --outfile dot-darwin-arm64 + + - name: Generate CLI release tag + if: steps.bot.outputs.skip != 'true' && steps.changesets.outputs.found == 'true' + id: cli_tag + run: | + VERSION=$(node -p "require('./apps/cli/package.json').version") + TAG="v${VERSION}" + echo "tag=$TAG" >> $GITHUB_OUTPUT + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - name: Create GitHub Release + if: steps.bot.outputs.skip != 'true' && steps.changesets.outputs.found == 'true' + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ steps.cli_tag.outputs.tag }} + name: Release ${{ steps.cli_tag.outputs.tag }} + files: | + dot-linux-x64 + dot-linux-arm64 + dot-darwin-x64 + dot-darwin-arm64 + generate_release_notes: true diff --git a/.gitignore b/.gitignore index 4a10cbc..a175c51 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ coverage/ .pnpm-store/ reference-repos/ docs/ +.cdm/ \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index b435682..b33f6f0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -13,10 +13,32 @@ Follow the contributor guidelines in `README.md`. - **Format check:** `pnpm format:check` - **Generate descriptors:** `pnpm generate-descriptors` - **Generate docs:** `pnpm docs` +- **Install CLI locally:** `pnpm cli:install` + +## CLI + +- The CLI lives at `apps/cli/` and installs as `dot`. Compiles to a standalone binary via `bun build --compile`. +- **Dev:** `bun run apps/cli/src/index.ts` (runs TypeScript directly) +- **Local install:** `pnpm cli:install` (builds all packages, compiles binary to `~/.polkadot/bin/dot`) +- **End-user install:** `bash install.sh` (downloads binary from GitHub Releases, uses `gh` for private repo auth) +- The CLI is `private: true` — not published to npm. Distributed as compiled binaries via GitHub Releases. +- Commands are separate files in `apps/cli/src/commands/`, each exporting a `Command` instance registered via `program.addCommand()`. +- Contract ABI (`cdm.json`) is managed by CDM — `postinstall` runs `cdm i` automatically if cdm is available. + +**Commands:** `init`, `remix`, `build`, `deploy`, `info`, `update` + +| Command | Purpose | +|---------|---------| +| `dot init` | Set up dev environment (Rust toolchain, gh CLI) and authenticate | +| `dot remix [domain]` | Fork an app — interactive picker if no domain, `--quest` flag (stubbed) | +| `dot build` | Detect and build contracts (Rust) and frontend | +| `dot deploy` | Deploy contracts + frontend to Bulletin. `--playground` to also publish to registry | +| `dot info ` | Show detailed app information from the registry | +| `dot update` | Self-update from GitHub Releases | ## Key Conventions -- All packages live in `packages//` and are scoped under `@polkadot-apps/`. +- All packages live in `packages//` and are scoped under `@polkadot-apps/`. The CLI lives in `apps/cli/`. - Use in-source testing (`if (import.meta.vitest)` blocks) for unit tests. Separate `tests/*.test.ts` files are for integration tests only. - Internal deps use `"workspace:*"`, shared versions use `"catalog:"` from `pnpm-workspace.yaml`. - Packages must be framework-agnostic pure TypeScript. No React/Vue imports in core packages. diff --git a/apps/cli/cdm.json b/apps/cli/cdm.json new file mode 100644 index 0000000..353bef4 --- /dev/null +++ b/apps/cli/cdm.json @@ -0,0 +1,248 @@ +{ + "targets": { + "acc2c3b5e912b762": { + "asset-hub": "wss://asset-hub-paseo-rpc.n.dwellir.com", + "bulletin": "https://paseo-ipfs.polkadot.io/ipfs" + } + }, + "dependencies": { + "acc2c3b5e912b762": { + "@example/playground-registry": "latest" + } + }, + "contracts": { + "acc2c3b5e912b762": { + "@example/playground-registry": { + "version": 6, + "address": "0x279585Cb8E8971e34520A3ebbda3E0C4D77C3d97", + "abi": [ + { + "type": "constructor", + "inputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "publish", + "inputs": [ + { + "name": "domain", + "type": "string" + }, + { + "name": "metadata_uri", + "type": "string" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "unpublish", + "inputs": [ + { + "name": "domain", + "type": "string" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "rateApp", + "inputs": [ + { + "name": "domain", + "type": "string" + }, + { + "name": "rating", + "type": "uint8" + }, + { + "name": "comment_uri", + "type": "string" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "removeRating", + "inputs": [ + { + "name": "domain", + "type": "string" + }, + { + "name": "reviewer", + "type": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "getContextId", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getAppCount", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getDomainAt", + "inputs": [ + { + "name": "index", + "type": "uint32" + } + ], + "outputs": [ + { + "name": "", + "type": "tuple", + "components": [ + { + "name": "isSome", + "type": "bool" + }, + { + "name": "value", + "type": "string" + } + ] + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getOwnerAppCount", + "inputs": [ + { + "name": "owner", + "type": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getOwnerDomainAt", + "inputs": [ + { + "name": "owner", + "type": "address" + }, + { + "name": "index", + "type": "uint32" + } + ], + "outputs": [ + { + "name": "", + "type": "tuple", + "components": [ + { + "name": "isSome", + "type": "bool" + }, + { + "name": "value", + "type": "string" + } + ] + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getSudo", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getMetadataUri", + "inputs": [ + { + "name": "domain", + "type": "string" + } + ], + "outputs": [ + { + "name": "", + "type": "tuple", + "components": [ + { + "name": "isSome", + "type": "bool" + }, + { + "name": "value", + "type": "string" + } + ] + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getOwner", + "inputs": [ + { + "name": "domain", + "type": "string" + } + ], + "outputs": [ + { + "name": "", + "type": "address" + } + ], + "stateMutability": "view" + } + ], + "metadataCid": "bafk2bzaceck7veaix4ttzyd6bmwlssgycrrlgilpat2c272nczzlrgnqy6fze" + } + } + } +} diff --git a/apps/cli/package.json b/apps/cli/package.json new file mode 100644 index 0000000..859fbf6 --- /dev/null +++ b/apps/cli/package.json @@ -0,0 +1,35 @@ +{ + "name": "@polkadot-apps/cli", + "description": "CLI for building and managing Polkadot apps", + "version": "0.1.0", + "private": true, + "type": "module", + "bin": { + "dot": "./src/index.ts" + }, + "scripts": { + "postinstall": "[ -d .cdm ] || (command -v cdm >/dev/null && cdm i || true)", + "build": "echo 'No build needed - compiled via bun'", + "compile": "bun build --compile src/index.ts --outfile dist/dot", + "clean": "rm -rf dist" + }, + "dependencies": { + "commander": "catalog:", + "@polkadot-apps/address": "workspace:*", + "@polkadot-apps/bulletin": "workspace:*", + "@polkadot-apps/chain-client": "workspace:*", + "@polkadot-apps/contracts": "workspace:*", + "@polkadot-apps/keys": "workspace:*", + "@polkadot-api/sdk-ink": "catalog:", + "@polkadot-apps/terminal": "workspace:*", + "@polkadot-apps/tx": "workspace:*", + "@polkadot-apps/utils": "workspace:*", + "bulletin-deploy": "catalog:", + "@polkadot-labs/hdkd-helpers": "catalog:", + "polkadot-api": "catalog:", + "ws": "catalog:" + }, + "devDependencies": { + "typescript": "catalog:" + } +} diff --git a/apps/cli/src/commands/build.ts b/apps/cli/src/commands/build.ts new file mode 100644 index 0000000..e4f0f87 --- /dev/null +++ b/apps/cli/src/commands/build.ts @@ -0,0 +1,75 @@ +import { Command } from "commander"; +import { execSync } from "node:child_process"; +import { hasContracts, getBuildCommand, ensureToolchain } from "../project.js"; +import { spinner, bold, dim, green, red } from "../ui.js"; + +/* @integration */ +export const buildCommand = new Command("build") + .description("Build contracts and frontend") + .option("--contracts-only", "Only build contracts") + .option("--frontend-only", "Only build frontend") + .action((opts) => { + let failed = false; + + // Ensure toolchain (quiet — only prints when installing) + { + let activeSpinner: ReturnType | null = null; + const results = ensureToolchain({ + onStep: (name, status, msg) => { + if (status === "installing") { + activeSpinner = spinner(name, msg ?? `Installing ${name}...`); + } else if (status === "ok" && activeSpinner) { + activeSpinner.succeed(msg ?? name); + activeSpinner = null; + } else if (status === "failed") { + activeSpinner?.fail(msg ?? `Failed to install ${name}`); + activeSpinner = null; + } + }, + }); + const failures = results.filter((r) => !r.ok); + if (failures.length > 0) { + for (const f of failures) { + console.log(` ${red("✖")} ${f.name}: ${f.error}`); + if (f.manualHint) console.log(` ${dim(f.manualHint)}`); + } + process.exitCode = 1; + return; + } + } + + // Contracts + if (!opts.frontendOnly && hasContracts()) { + const s = spinner("Contracts", "Building..."); + try { + execSync("cargo pvm-contract build --release", { stdio: "inherit" }); + s.succeed("Contracts built"); + } catch { + s.fail("Contract build failed"); + failed = true; + } + } else if (!opts.frontendOnly) { + console.log(` ${dim("No contracts detected (no contracts/ or Cargo.toml)")}`); + } + + // Frontend + const buildCmd = getBuildCommand(); + if (!opts.contractsOnly && buildCmd) { + const s = spinner("Frontend", "Building..."); + try { + execSync(buildCmd, { stdio: "inherit" }); + s.succeed("Frontend built"); + } catch { + s.fail("Frontend build failed"); + failed = true; + } + } else if (!opts.contractsOnly && !buildCmd) { + console.log(` ${dim("No frontend detected (no build script in package.json)")}`); + } + + if (failed) { + process.exitCode = 1; + } else { + console.log(`${green("✔")} ${bold("Build complete")}`); + } + }); diff --git a/apps/cli/src/commands/deploy.ts b/apps/cli/src/commands/deploy.ts new file mode 100644 index 0000000..7332b0a --- /dev/null +++ b/apps/cli/src/commands/deploy.ts @@ -0,0 +1,735 @@ +import { Command } from "commander"; +import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs"; +import { resolve, relative } from "node:path"; +import { execSync, execFileSync } from "node:child_process"; +import { createInterface } from "node:readline/promises"; +import { computeCid, BulletinClient } from "@polkadot-apps/bulletin"; +import { getBalance, formatBalance } from "@polkadot-apps/utils"; +import type { PolkadotSigner } from "polkadot-api"; +import { connect, type Connection } from "../connection.js"; +import { getSessionSigner } from "../utils/session.js"; +import { TAGS } from "../config.js"; +import { + loadProjectConfig, + getGitRemoteUrl, + getGitBranch, + resolveSigner, + loadMnemonic, + readReadme, + hasContracts, + getBuildCommand, + ensureToolchain, +} from "../project.js"; +import { spinner, bold, dim, cyan, green, red, yellow } from "../ui.js"; + +const MIN_BALANCE = 1_000_000_000n; // 0.1 PAS — enough for a few txs + +// --------------------------------------------------------------------------- +// Read config: playground:* fields from package.json, with dot.json fallback +// --------------------------------------------------------------------------- + +interface PlaygroundConfig { + domain?: string; + name?: string; + description?: string; + tag?: string; + icon?: string; + branch?: string; +} + +function loadPlaygroundConfig(): PlaygroundConfig { + const pkgPath = resolve(process.cwd(), "package.json"); + const dotJson = loadProjectConfig(); + try { + const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")); + return { + domain: pkg["playground:domain"] ?? dotJson.domain, + name: pkg["playground:name"] ?? pkg.name ?? dotJson.name, + description: pkg["playground:description"] ?? pkg.description ?? dotJson.description, + tag: pkg["playground:tag"] ?? dotJson.tag, + icon: pkg["playground:icon"] ?? dotJson.icon, + branch: pkg["playground:branch"] ?? dotJson.branch, + }; + } catch { + // No package.json — use dot.json only + return { + domain: dotJson.domain, + name: dotJson.name, + description: dotJson.description, + tag: dotJson.tag, + icon: dotJson.icon, + branch: dotJson.branch, + }; + } +} + +function hasDistDir(): boolean { + return existsSync(resolve(process.cwd(), "dist")); +} + +// --------------------------------------------------------------------------- +// Signer resolution: QR session → --suri → mnemonic file +// --------------------------------------------------------------------------- + +/* @integration */ +async function resolveDeploySigner( + chain: string, + suri?: string, +): Promise<{ signer: PolkadotSigner; origin: string }> { + // 1. Explicit --suri flag (dev signing) + if (suri) { + return resolveSigner(chain, suri); + } + + // 2. QR session from mobile wallet + const session = await getSessionSigner(); + if (session) { + console.log(` ${dim("Signing via mobile wallet")}`); + return session; + } + + // 3. Mnemonic file fallback + return resolveSigner(chain); +} + +// --------------------------------------------------------------------------- +// Contract name detection & rename prompt +// --------------------------------------------------------------------------- + +interface ContractInfo { + file: string; + name: string; +} + +function detectContractNames(): ContractInfo[] { + const results: ContractInfo[] = []; + const cwd = process.cwd(); + + const scanDir = (dir: string) => { + if (!existsSync(dir)) return; + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const full = resolve(dir, entry.name); + if (entry.isDirectory() && entry.name !== "target") scanDir(full); + else if (entry.name.endsWith(".rs")) { + const content = readFileSync(full, "utf-8"); + const match = content.match(/#\[pvm::contract\(cdm\s*=\s*"([^"]+)"\)/); + if (match) results.push({ file: full, name: match[1] }); + } + } + }; + + scanDir(resolve(cwd, "contracts")); + const rootLib = resolve(cwd, "lib.rs"); + if (existsSync(rootLib)) { + const content = readFileSync(rootLib, "utf-8"); + const match = content.match(/#\[pvm::contract\(cdm\s*=\s*"([^"]+)"\)/); + if (match) results.push({ file: rootLib, name: match[1] }); + } + + return results; +} + +type ContractAction = "deploy" | "rename" | "skip"; + +/* @integration */ +async function promptContractAction(contracts: ContractInfo[]): Promise { + const cwd = process.cwd(); + console.log(); + console.log(` ${bold("Contracts detected:")}`); + for (const c of contracts) { + console.log(` ${cyan(c.name)} ${dim(relative(cwd, c.file))}`); + } + console.log(); + console.log( + ` ${yellow("These contract names may already be registered by another publisher.")}`, + ); + console.log(); + console.log(` ${bold("d")} Deploy as-is ${dim("(only works if you own these names)")}`); + console.log( + ` ${bold("r")} Rename ${dim("(choose new @org/name for each contract)")}`, + ); + console.log(` ${bold("s")} Skip ${dim("(reuse existing on-chain contracts)")}`); + console.log(); + + const rl = createInterface({ input: process.stdin, output: process.stdout }); + const answer = await rl.question(` Choice [d/r/s]: `); + rl.close(); + + switch (answer.trim().toLowerCase()) { + case "r": + return "rename"; + case "s": + return "skip"; + default: + return "deploy"; + } +} + +/* @integration */ +async function renameContracts(contracts: ContractInfo[]): Promise { + const cfg = loadPlaygroundConfig(); + const suggestedOrg = cfg.domain ? `@${cfg.domain.replace(/\.dot$/, "")}` : undefined; + + const rl = createInterface({ input: process.stdin, output: process.stdout }); + console.log(); + if (suggestedOrg) { + console.log(` ${dim(`Suggested org from playground:domain: ${bold(suggestedOrg)}`)}`); + } + + for (const c of contracts) { + const parts = c.name.split("/"); + const shortName = parts.slice(1).join("/"); + const defaultNew = suggestedOrg ? `${suggestedOrg}/${shortName}` : undefined; + const hint = defaultNew ? ` [${defaultNew}]` : ""; + + const answer = await rl.question(` New name for ${cyan(c.name)}${dim(hint)}: `); + const newName = answer.trim() || defaultNew; + + if (newName && newName !== c.name) { + const content = readFileSync(c.file, "utf-8"); + const updated = content.replace(`cdm = "${c.name}"`, `cdm = "${newName}"`); + writeFileSync(c.file, updated); + console.log(` ${green("✔")} ${dim(c.name)} → ${cyan(newName)}`); + } else if (!newName) { + console.log(` ${dim(" Keeping")} ${c.name}`); + } + } + + rl.close(); + console.log(); +} + +// --------------------------------------------------------------------------- +// Command +// --------------------------------------------------------------------------- + +/* @integration */ +export const deployCommand = new Command("deploy") + .description("Deploy contracts, frontend, and optionally publish to playground registry") + .option("-n, --name ", "Target chain", "paseo") + .option("--suri ", "Signer secret URI (e.g. //Alice for dev)") + .option("--contracts", "Include contract build & deploy") + .option("--skip-frontend", "Skip frontend build & deploy") + .option("--playground", "Also publish metadata to the playground registry") + .option("--bootstrap", "Also deploy the ContractRegistry (contracts only)") + .option("--domain ", "App domain (overrides package.json)") + .option("--app-name ", "Display name (overrides package.json)") + .option("--description ", "Short description (overrides package.json)") + .option("--repo ", "Source repository URL (overrides package.json)") + .option("--branch ", "Git branch (overrides package.json)") + .option("--tag ", `Category: ${TAGS.join(", ")}`) + .option("--icon ", "Path to icon image file") + .option("-y, --yes", "Skip interactive prompts (deploy contracts as-is)") + .option("--mobile-signer", "Use QR mobile wallet signer for bulletin-deploy (experimental)") + .action(async (opts) => { + const chain = opts.name; + let failed = false; + let conn: Connection | undefined; + + try { + // ── Ensure toolchain (quiet — only prints when installing) ──────── + { + let activeSpinner: ReturnType | null = null; + const results = ensureToolchain({ + onStep: (name, status, msg) => { + if (status === "installing") { + activeSpinner = spinner(name, msg ?? `Installing ${name}...`); + } else if (status === "ok" && activeSpinner) { + activeSpinner.succeed(msg ?? name); + activeSpinner = null; + } else if (status === "failed") { + activeSpinner?.fail(msg ?? `Failed to install ${name}`); + activeSpinner = null; + } + }, + }); + const failures = results.filter((r) => !r.ok); + if (failures.length > 0) { + for (const f of failures) { + console.log(` ${red("✖")} ${f.name}: ${f.error}`); + if (f.manualHint) console.log(` ${dim(f.manualHint)}`); + } + console.log(); + console.log( + `${red("Missing dependencies.")} Run ${bold("dot init")} to set up your environment.`, + ); + process.exit(1); + } + } + + // ── Resolve config from package.json playground:* fields ───────── + const config = loadPlaygroundConfig(); + const domain = opts.domain ?? config.domain; + if (!domain) { + console.error( + 'No domain specified. Use --domain or set "playground:domain" in package.json (or dot.json).', + ); + process.exit(1); + } + const fullDomain = domain.endsWith(".dot") ? domain : `${domain}.dot`; + + console.log(); + console.log(` ${bold("dot deploy")}`); + console.log(` ${dim("Chain:")} ${chain}`); + console.log(` ${dim("Domain:")} ${bold(fullDomain)}`); + if (opts.playground) console.log(` ${dim("Registry:")} enabled`); + console.log(); + + // ── Resolve signer ──────────────────────────────────────────────── + const s0 = spinner("Signer", "Resolving..."); + let signer: PolkadotSigner; + let origin: string; + let signerMethod: string; + try { + if (opts.mobileSigner) { + // QR session from mobile wallet + const resolved = await resolveDeploySigner(chain); + signer = resolved.signer; + origin = resolved.origin; + signerMethod = "QR mobile wallet"; + } else if (opts.suri) { + const resolved = resolveSigner(chain, opts.suri); + signer = resolved.signer; + origin = resolved.origin; + signerMethod = `dev SURI (${opts.suri})`; + } else { + // Mnemonic file → Alice fallback (matches bulletin-deploy path) + let mnemonic: string | undefined; + try { + mnemonic = loadMnemonic(chain); + } catch { + // No accounts file — fall back to dev mnemonic + } + if (mnemonic) { + const resolved = resolveSigner(chain); + signer = resolved.signer; + origin = resolved.origin; + signerMethod = "mnemonic file"; + } else { + const resolved = resolveSigner(chain, "//Alice"); + signer = resolved.signer; + origin = resolved.origin; + signerMethod = "dev mnemonic (Alice)"; + } + } + s0.succeed(`Signer ready`); + console.log(` ${dim("Address:")} ${cyan(origin)}`); + console.log(` ${dim("Method:")} ${signerMethod}`); + } catch (err) { + s0.fail(err instanceof Error ? err.message : "Failed to resolve signer"); + process.exit(1); + } + + // ── Balance check on Asset Hub ─────────────────────────────────── + const s0b = spinner("Balance", "Checking Asset Hub balance..."); + try { + conn = await connect(chain); + const balance = await getBalance(conn.assetHub, origin); + const display = formatBalance(balance.free, { symbol: "PAS", maxDecimals: 4 }); + if (balance.free === 0n) { + s0b.fail(`Account has no funds (${display})`); + console.log(` ${dim("Fund this account on Asset Hub before deploying.")}`); + process.exit(1); + } else if (balance.free < MIN_BALANCE) { + s0b.fail(`Low balance: ${display}`); + console.log( + ` ${yellow("!")} Balance may be insufficient for deployment fees.`, + ); + } else { + s0b.succeed(`${display}`); + } + } catch (err) { + s0b.fail( + `Could not check balance: ${err instanceof Error ? err.message : String(err)}`, + ); + // Non-fatal — proceed with deploy, it will fail later if truly unfunded + } + console.log(); + + // ── Step 1: Contracts ───────────────────────────────────────────── + if (opts.contracts && hasContracts()) { + let skipContracts = false; + + if (!opts.yes) { + const contracts = detectContractNames(); + const expectedOrg = `@${domain.replace(/\.dot$/, "")}`; + const allMatch = + contracts.length > 0 && + contracts.every((c) => c.name.startsWith(expectedOrg + "/")); + + if (contracts.length > 0 && !allMatch) { + const action = await promptContractAction(contracts); + if (action === "skip") { + skipContracts = true; + console.log(` ${dim("Skipping contract deployment.")}`); + } else if (action === "rename") { + await renameContracts(contracts); + } + } + } + + if (!skipContracts) { + const s = spinner("Contracts", "Building & deploying..."); + try { + const cdmArgs = ["deploy", "-n", chain]; + if (opts.bootstrap) cdmArgs.push("--bootstrap"); + if (opts.suri) cdmArgs.push("--suri", opts.suri); + execFileSync("cdm", cdmArgs, { stdio: "inherit" }); + s.succeed("Contracts deployed"); + } catch { + s.fail("Contract deployment failed"); + failed = true; + } + } + } else if (opts.contracts) { + console.log(` ${dim("No contracts detected")}`); + } + + // ── Step 2: Playground registry publish (--playground only) ─────── + // Runs before frontend deploy so QR session is still fresh. + if (opts.playground) { + try { + const gitRemote = getGitRemoteUrl(); + + const appName = opts.appName ?? config.name; + const description = opts.description ?? config.description; + const repository = opts.repo ?? gitRemote; + const branch = opts.branch ?? config.branch ?? getGitBranch(); + const tag = opts.tag ?? config.tag; + const iconPath = opts.icon ?? config.icon; + + if (tag && !(TAGS as readonly string[]).includes(tag)) { + throw new Error(`Invalid tag "${tag}". Must be one of: ${TAGS.join(", ")}`); + } + + let iconBytes: Uint8Array | undefined; + let iconCid: string | undefined; + if (iconPath) { + iconBytes = new Uint8Array(readFileSync(resolve(process.cwd(), iconPath))); + iconCid = computeCid(iconBytes); + } + + const readme = readReadme(); + const metadata: Record = {}; + if (appName) metadata.name = appName; + if (description) metadata.description = description; + if (repository) metadata.repository = repository; + if (branch) metadata.branch = branch; + if (tag) metadata.tag = tag; + if (iconCid) metadata.icon_cid = iconCid; + if (readme) metadata.readme = readme; + + if (Object.keys(metadata).length === 0) { + throw new Error( + "No metadata. Add playground:* fields to package.json or pass --app-name, --description, etc.", + ); + } + + const metadataBytes = new TextEncoder().encode(JSON.stringify(metadata)); + const metadataCid = computeCid(metadataBytes); + + console.log(` ${dim("Connecting to registry...")}`); + if (!conn) conn = await connect(chain); + + // Signing must be sequential (QR session handles one at a time), + // but transactions confirm in parallel after signing. + // A mutex ensures signing requests don't overlap. + let signingQueue: Promise = Promise.resolve(); + const queuedSigner: typeof signer = { + publicKey: signer.publicKey, + signTx(...args: Parameters) { + const prev = signingQueue; + const result = prev.then(() => signer.signTx(...args)); + signingQueue = result.catch(() => {}); + return result; + }, + signBytes(...args: Parameters) { + const prev = signingQueue; + const result = prev.then(() => signer.signBytes(...args)); + signingQueue = result.catch(() => {}); + return result; + }, + }; + + const s1 = spinner("Upload metadata", ""); + const s2 = spinner("Playground register", ""); + + const bulletinPromise = (async () => { + const client = await BulletinClient.create(chain); + const items: { data: Uint8Array; label: string }[] = []; + if (iconBytes) items.push({ data: iconBytes, label: "icon" }); + items.push({ data: metadataBytes, label: "metadata" }); + await client.batchUpload(items, queuedSigner); + if (typeof (client as any).destroy === "function") + (client as any).destroy(); + s1.succeed(); + })(); + + const registryPromise = (async () => { + const result = await conn!.registry.publish.tx(fullDomain, metadataCid, { + signer: queuedSigner, + origin, + }); + if (!result.ok) { + const errDetail = result.dispatchError + ? JSON.stringify(result.dispatchError, (_: string, v: unknown) => + typeof v === "bigint" ? v.toString() : v, + ) + : "Transaction failed"; + throw new Error(errDetail); + } + s2.succeed(); + })(); + + const results = await Promise.allSettled([bulletinPromise, registryPromise]); + const failures = results.filter( + (r): r is PromiseRejectedResult => r.status === "rejected", + ); + if (failures.length > 0) { + if (!s1.done) s1.fail(); + if (!s2.done) s2.fail(); + throw failures[0].reason; + } + } catch (err) { + console.log( + `\n ${red("✖")} ${bold("Registry publish failed:")} ${err instanceof Error ? err.message : String(err)}`, + ); + failed = true; + } + } + + // Close registry connection before the blocking frontend build so + // stale WebSocket messages don't cause JSON parse errors. + if (conn) { + conn.destroy(); + conn = undefined; + } + + // ── Step 3: Frontend build + deploy to Bulletin ─────────────────── + const buildCmd = getBuildCommand(); + if (opts.skipFrontend) { + console.log(` ${dim("Skipping frontend (--skip-frontend)")}`); + } else if (!buildCmd) { + console.log(` ${dim("No frontend detected (no build script)")}`); + } else { + const s = spinner("Frontend", `Building (${buildCmd})...`); + try { + execSync(buildCmd, { stdio: "inherit" }); + + if (!hasDistDir()) { + throw new Error("Build did not produce a dist/ directory"); + } + + s.update(`Deploying ${fullDomain} to Bulletin...`); + const { deploy: bulletinDeploy } = await import("bulletin-deploy"); + + const deployOpts: any = {}; + let bulletinSignerMethod: string; + + if (opts.mobileSigner || opts.suri) { + // Use the already-resolved signer for mobile wallet or SURI + deployOpts.signer = signer; + deployOpts.signerAddress = origin; + bulletinSignerMethod = opts.mobileSigner + ? "QR mobile signer (experimental)" + : `dev SURI (${opts.suri})`; + } else { + // Try mnemonic from accounts file first + let mnemonic: string | undefined; + try { + mnemonic = loadMnemonic(chain); + } catch { + // No accounts file — fall back to dev mnemonic + } + + if (mnemonic) { + deployOpts.mnemonic = mnemonic; + bulletinSignerMethod = "mnemonic file"; + } else { + // Default to dev mnemonic (Alice) + const { DEV_PHRASE } = await import("@polkadot-labs/hdkd-helpers"); + deployOpts.mnemonic = DEV_PHRASE; + bulletinSignerMethod = "dev mnemonic (Alice)"; + } + } + console.log(` ${dim("Signer:")} ${bulletinSignerMethod}`); + + const result = await bulletinDeploy( + "./dist", + fullDomain.replace(".dot", ""), + deployOpts, + ); + s.succeed( + `Frontend deployed to ${bold(result.fullDomain)} (${dim(result.cid)})`, + ); + } catch (err) { + s.fail(err instanceof Error ? err.message : "Frontend deployment failed"); + if (err instanceof Error && err.stack) { + console.log(` ${dim(err.stack.split("\n").slice(1, 4).join("\n "))}`); + } + failed = true; + } + } + + // ── Done ────────────────────────────────────────────────────────── + console.log(); + if (failed) { + console.log(`${red("Some steps failed.")} Check the output above.`); + process.exit(1); + } else { + console.log(`${green("✔")} Deploy complete.`); + process.exit(0); + } + } finally { + conn?.destroy(); + } + }); + +if (import.meta.vitest) { + const { test, expect, describe } = import.meta.vitest; + const { mkdtempSync, writeFileSync: _writeFile, mkdirSync, rmSync } = await import("node:fs"); + const { tmpdir } = await import("node:os"); + const { join } = await import("node:path"); + + describe("loadPlaygroundConfig", () => { + test("reads playground:* fields from package.json", () => { + const dir = mkdtempSync(join(tmpdir(), "cli-test-")); + _writeFile( + join(dir, "package.json"), + JSON.stringify({ + name: "my-app", + "playground:domain": "test", + "playground:tag": "defi", + "playground:description": "A test app", + }), + ); + const orig = process.cwd(); + process.chdir(dir); + const config = loadPlaygroundConfig(); + expect(config.domain).toBe("test"); + expect(config.tag).toBe("defi"); + expect(config.description).toBe("A test app"); + expect(config.name).toBe("my-app"); // falls back to pkg.name + process.chdir(orig); + rmSync(dir, { recursive: true }); + }); + + test("falls back to dot.json when no package.json", () => { + const dir = mkdtempSync(join(tmpdir(), "cli-test-")); + _writeFile( + join(dir, "dot.json"), + JSON.stringify({ domain: "remix-app", name: "Remix App", tag: "gaming" }), + ); + const orig = process.cwd(); + process.chdir(dir); + const config = loadPlaygroundConfig(); + expect(config.domain).toBe("remix-app"); + expect(config.name).toBe("Remix App"); + expect(config.tag).toBe("gaming"); + process.chdir(orig); + rmSync(dir, { recursive: true }); + }); + + test("package.json playground:* takes precedence over dot.json", () => { + const dir = mkdtempSync(join(tmpdir(), "cli-test-")); + _writeFile( + join(dir, "package.json"), + JSON.stringify({ "playground:domain": "pkg-domain" }), + ); + _writeFile(join(dir, "dot.json"), JSON.stringify({ domain: "dot-domain" })); + const orig = process.cwd(); + process.chdir(dir); + expect(loadPlaygroundConfig().domain).toBe("pkg-domain"); + process.chdir(orig); + rmSync(dir, { recursive: true }); + }); + + test("dot.json fills gaps in package.json", () => { + const dir = mkdtempSync(join(tmpdir(), "cli-test-")); + _writeFile( + join(dir, "package.json"), + JSON.stringify({ "playground:domain": "pkg-domain" }), + ); + _writeFile(join(dir, "dot.json"), JSON.stringify({ tag: "defi", name: "My App" })); + const orig = process.cwd(); + process.chdir(dir); + const config = loadPlaygroundConfig(); + expect(config.domain).toBe("pkg-domain"); + expect(config.tag).toBe("defi"); + expect(config.name).toBe("My App"); + process.chdir(orig); + rmSync(dir, { recursive: true }); + }); + + test("returns empty when no package.json or dot.json", () => { + const dir = mkdtempSync(join(tmpdir(), "cli-test-")); + const orig = process.cwd(); + process.chdir(dir); + const config = loadPlaygroundConfig(); + expect(config.domain).toBeUndefined(); + process.chdir(orig); + rmSync(dir, { recursive: true }); + }); + + test("prefers playground:name over name", () => { + const dir = mkdtempSync(join(tmpdir(), "cli-test-")); + _writeFile( + join(dir, "package.json"), + JSON.stringify({ name: "pkg-name", "playground:name": "Display Name" }), + ); + const orig = process.cwd(); + process.chdir(dir); + expect(loadPlaygroundConfig().name).toBe("Display Name"); + process.chdir(orig); + rmSync(dir, { recursive: true }); + }); + }); + + describe("hasDistDir", () => { + test("returns false when no dist/", () => { + const dir = mkdtempSync(join(tmpdir(), "cli-test-")); + const orig = process.cwd(); + process.chdir(dir); + expect(hasDistDir()).toBe(false); + process.chdir(orig); + rmSync(dir, { recursive: true }); + }); + + test("returns true when dist/ exists", () => { + const dir = mkdtempSync(join(tmpdir(), "cli-test-")); + mkdirSync(join(dir, "dist")); + const orig = process.cwd(); + process.chdir(dir); + expect(hasDistDir()).toBe(true); + process.chdir(orig); + rmSync(dir, { recursive: true }); + }); + }); + + describe("detectContractNames", () => { + test("returns empty in directory without contracts", () => { + const dir = mkdtempSync(join(tmpdir(), "cli-test-")); + const orig = process.cwd(); + process.chdir(dir); + expect(detectContractNames()).toEqual([]); + process.chdir(orig); + rmSync(dir, { recursive: true }); + }); + + test("detects #[pvm::contract(cdm = ...)] in Rust files", () => { + const dir = mkdtempSync(join(tmpdir(), "cli-test-")); + mkdirSync(join(dir, "contracts")); + _writeFile( + join(dir, "contracts", "lib.rs"), + '#[pvm::contract(cdm = "@myorg/counter")]\npub fn main() {}', + ); + const orig = process.cwd(); + process.chdir(dir); + const names = detectContractNames(); + expect(names.length).toBe(1); + expect(names[0].name).toBe("@myorg/counter"); + process.chdir(orig); + rmSync(dir, { recursive: true }); + }); + }); +} diff --git a/apps/cli/src/commands/info.ts b/apps/cli/src/commands/info.ts new file mode 100644 index 0000000..bcbc810 --- /dev/null +++ b/apps/cli/src/commands/info.ts @@ -0,0 +1,71 @@ +import { Command } from "commander"; +import { connect, fetchIpfs, unwrapOption } from "../connection.js"; +import { type AppMetadata } from "../config.js"; +import { spinner, bold, dim, cyan } from "../ui.js"; + +/* @integration */ +export const infoCommand = new Command("info") + .description("Show detailed information about an app") + .argument("", "App domain (e.g. my-app.dot)") + .option("-n, --name ", "Chain to connect to", "paseo") + .option("--ipfs-gateway-url ", "Override IPFS gateway URL") + .action(async (rawDomain: string, opts) => { + const domain = rawDomain.endsWith(".dot") ? rawDomain : `${rawDomain}.dot`; + const s = spinner("Info", "Connecting..."); + let conn; + try { + conn = await connect(opts.name); + const gateway = opts.ipfsGatewayUrl ?? conn.ipfsGateway; + + s.update(`Querying ${domain}...`); + + const [metaRes, ownerRes] = await Promise.all([ + conn.registry.getMetadataUri.query(domain), + conn.registry.getOwner.query(domain), + ]); + + const owner = ownerRes.success ? String(ownerRes.value) : undefined; + const cid = unwrapOption(metaRes.success ? metaRes.value : undefined); + + if (!cid && (!owner || owner === "0x0000000000000000000000000000000000000000")) { + s.fail(`App "${domain}" not found in registry.`); + process.exitCode = 1; + return; + } + + let metadata: AppMetadata | undefined; + if (cid) { + s.update("Fetching metadata from IPFS..."); + try { + metadata = await fetchIpfs(cid, gateway); + } catch {} + } + + s.succeed(domain); + console.log(); + + const lines: [string, string][] = [ + ["Domain", bold(domain)], + ["Name", metadata?.name ?? dim("not set")], + ["Description", metadata?.description ?? dim("not set")], + ["Owner", owner ? cyan(owner) : dim("unknown")], + ["Repository", metadata?.repository ?? dim("not set")], + ["Tag", metadata?.tag ?? dim("not set")], + ["Metadata CID", cid ?? dim("none")], + ]; + + if (metadata?.icon_cid) { + lines.push(["Icon", `${gateway}/${metadata.icon_cid}`]); + } + + const maxLabel = Math.max(...lines.map(([l]) => l.length)); + for (const [label, value] of lines) { + console.log(` ${dim(label.padEnd(maxLabel))} ${value}`); + } + } catch (err) { + s.fail(err instanceof Error ? err.message : String(err)); + process.exitCode = 1; + } finally { + conn?.destroy(); + } + }); diff --git a/apps/cli/src/commands/init.ts b/apps/cli/src/commands/init.ts new file mode 100644 index 0000000..c0ffc31 --- /dev/null +++ b/apps/cli/src/commands/init.ts @@ -0,0 +1,321 @@ +import { Command } from "commander"; +import { ss58Encode } from "@polkadot-apps/address"; +import { getChainAPI } from "@polkadot-apps/chain-client"; +import { + createTerminalAdapter, + renderQrCode, + createSessionSigner, + DEFAULT_METADATA_URL, + DEFAULT_PEOPLE_ENDPOINTS, +} from "@polkadot-apps/terminal"; +import type { PairingStatus, AttestationStatus } from "@polkadot-apps/terminal"; +import { formatBalance } from "@polkadot-apps/utils"; +import { ensureToolchain, commandExists, isGhAuthenticated } from "../project.js"; +import { spinner, bold, dim, green, red, yellow } from "../ui.js"; +import { + fetchAccountStatus, + needsFunding, + fundFromAlice, + mapAccount, + grantBulletinAllowance, + FUND_AMOUNT, + BULLETIN_TRANSACTIONS, + BULLETIN_BYTES, +} from "../utils/account-handler.js"; + +if (import.meta.vitest) { + const { test, expect, describe } = import.meta.vitest; + + describe("commandExists", () => { + test("returns true for common commands", () => { + expect(commandExists("node")).toBe(true); + expect(commandExists("git")).toBe(true); + }); + test("returns false for nonexistent commands", () => { + expect(commandExists("definitely-not-a-real-command-xyz")).toBe(false); + }); + }); + + describe("isGhAuthenticated", () => { + test("returns a boolean", () => { + expect(typeof isGhAuthenticated()).toBe("boolean"); + }); + }); +} + +// --------------------------------------------------------------------------- +// QR Login +// --------------------------------------------------------------------------- + +/* @integration */ +async function doQrLogin(): Promise { + const adapter = createTerminalAdapter({ + appId: "dot-cli", + metadataUrl: DEFAULT_METADATA_URL, + endpoints: DEFAULT_PEOPLE_ENDPOINTS, + }); + + // Check for existing session + // Wait for sessions to load from disk — first emission is often empty + const existingSessions = await new Promise((resolve) => { + let resolved = false; + let unsub: (() => void) | null = null; + unsub = adapter.sessions.sessions.subscribe((sessions) => { + if (resolved) return; + if (sessions.length > 0) { + resolved = true; + queueMicrotask(() => unsub?.()); + resolve(sessions); + } + }); + // If no sessions arrive within 3s, assume none exist + setTimeout(() => { + if (resolved) return; + resolved = true; + unsub?.(); + resolve([]); + }, 3000); + }); + + if (existingSessions.length > 0) { + const session = existingSessions[0]; + const pubkey = new Uint8Array(session.remoteAccount.accountId); + const ss58 = ss58Encode(pubkey); + console.log(` ${green("✔")} Authenticated`); + console.log(` ${dim("Address:")} ${ss58}`); + return ss58; + } + + // No existing session — start QR pairing + console.log(); + console.log(` ${bold("Scan with the Polkadot mobile app to log in:")}`); + console.log(); + + let qrShown = false; + const unsubPairing = adapter.sso.pairingStatus.subscribe((status: PairingStatus) => { + if (status.step === "pairing" && !qrShown) { + qrShown = true; + renderQrCode(status.payload).then((qr) => { + console.log(qr); + }); + } else if (status.step === "finished") { + console.log(` ${green("✔")} Paired with Polkadot App`); + } else if (status.step === "pairingError") { + console.log(` ${dim("Pairing error:")} ${status.message}`); + } + }); + + const unsubAttestation = adapter.sso.attestationStatus.subscribe( + (status: AttestationStatus) => { + if (status.step === "attestation") { + const s = spinner("Attestation", `Registering on-chain (${status.username})...`); + // Store spinner ref for cleanup — attestation can take a while + (adapter as any)._attestSpinner = s; + } else if (status.step === "finished") { + (adapter as any)._attestSpinner?.succeed("Attestation complete"); + } else if (status.step === "attestationError") { + (adapter as any)._attestSpinner?.fail(`Attestation failed: ${status.message}`); + } + }, + ); + + const result = await adapter.sso.authenticate(); + + unsubPairing(); + unsubAttestation(); + + let address: string | null = null; + result.match( + (session) => { + if (session) { + const pubkey = new Uint8Array(session.remoteAccount.accountId); + address = ss58Encode(pubkey); + console.log(); + console.log(` ${green("✔")} ${bold("Logged in successfully!")}`); + } else { + console.log(` ${dim("Login cancelled.")}`); + } + }, + (error) => { + console.log(` ${dim("Login failed:")} ${error.message}`); + }, + ); + + // Wait for session to persist to disk before destroying + if (address) { + await new Promise((resolve) => { + let resolved = false; + let unsub: (() => void) | null = null; + unsub = adapter.sessions.sessions.subscribe((sessions) => { + if (sessions.length > 0 && !resolved) { + resolved = true; + // Defer unsubscribe — callback may fire synchronously before unsub is assigned + queueMicrotask(() => unsub?.()); + resolve(); + } + }); + if (resolved) return; // Already resolved synchronously + setTimeout(() => { + if (!resolved) { + resolved = true; + unsub?.(); + resolve(); + } + }, 3000); + }); + } + + // Note: adapter.destroy() has a bug where it disconnects the WebSocket + // before unsubscribing statement store listeners, causing async + // DestroyedError noise. Skip it — process.exit in the caller cleans up. + return address; +} + +// --------------------------------------------------------------------------- +// Account Setup +// --------------------------------------------------------------------------- + +/* @integration */ +async function ensureAccountFunded(address: string): Promise { + const s = spinner("Account", "Fetching account status..."); + + let client; + try { + client = await getChainAPI("paseo"); + const { balance, mapped, auth } = await fetchAccountStatus(client, address); + + s.succeed("Account status"); + + // ── Display ────────────────────────────────────────────────── + console.log(` Address ${bold(address)}`); + console.log( + ` Asset Hub ${balance.free > 0n ? green(formatBalance(balance.free, { symbol: "PAS", maxDecimals: 4 })) : red("0 PAS")}`, + ); + console.log(` Mapped ${mapped ? green("yes") : red("no")}`); + if (auth.authorized) { + const mb = (Number(auth.remainingBytes) / 1_000_000).toFixed(1); + console.log( + ` Bulletin ${green(`${auth.remainingTransactions} txns`)} ${green(`${mb} MB`)}`, + ); + } else { + console.log(` Bulletin ${dim("no allowance")}`); + } + + // ── Fund if needed ─────────────────────────────────────────── + if (needsFunding(balance)) { + console.log(); + const fundSpinner = spinner( + "Fund", + `Transferring ${formatBalance(FUND_AMOUNT, { symbol: "PAS" })} from Alice...`, + ); + try { + await fundFromAlice(client, address); + fundSpinner.succeed(`Funded ${formatBalance(FUND_AMOUNT, { symbol: "PAS" })}`); + } catch (err) { + fundSpinner.fail("Failed to fund account"); + console.log(` ${dim(String(err))}`); + return; + } + } + + // ── Map if needed ──────────────────────────────────────────── + if (!mapped) { + console.log(); + const mapSpinner = spinner("Map", "Mapping account for Revive pallet..."); + try { + await mapAccount(client, address); + mapSpinner.succeed("Account mapped"); + } catch (err) { + mapSpinner.fail("Failed to map account"); + console.log(` ${dim(String(err))}`); + } + } + + // ── Bulletin allowance if needed ───────────────────────────── + if (!auth.authorized) { + console.log(); + const blSpinner = spinner("Bulletin", "Granting bulletin allowance from Alice..."); + try { + await grantBulletinAllowance(client, address); + const mb = (Number(BULLETIN_BYTES) / 1_000_000).toFixed(0); + blSpinner.succeed(`Authorized — ${BULLETIN_TRANSACTIONS} txns, ${mb} MB`); + } catch (err) { + blSpinner.fail("Failed to grant bulletin allowance"); + console.log(` ${dim(String(err))}`); + } + } + } catch (err) { + s.fail("Failed to fetch account status"); + console.log(` ${dim(String(err))}`); + } finally { + client?.destroy(); + } +} + +// --------------------------------------------------------------------------- +// Command +// --------------------------------------------------------------------------- + +/* @integration */ +export const initCommand = new Command("init") + .description("Set up your development environment and authenticate") + .option("--skip-toolchain", "Skip Rust toolchain setup") + .option("--skip-auth", "Skip authentication") + .action(async (opts) => { + console.log(); + console.log(` ${bold("dot init")} — Setting up your development environment`); + console.log(); + + // ── Step 1: Toolchain ──────────────────────────────────────── + if (!opts.skipToolchain) { + let activeSpinner: ReturnType | null = null; + + ensureToolchain({ + verbose: true, + onStep: (name, status, msg) => { + if (status === "ok") { + activeSpinner?.succeed(msg ?? name); + activeSpinner = null; + if (!msg) console.log(` ${green("✔")} ${name}`); + } else if (status === "installing") { + activeSpinner = spinner(name, msg ?? `Installing ${name}...`); + } else if (status === "failed") { + activeSpinner?.fail(msg ?? `Failed to install ${name}`); + activeSpinner = null; + } + }, + }); + + // GitHub CLI check (advisory, not auto-installed) + if (!commandExists("gh")) { + console.log(` ${yellow("!")} GitHub CLI not found`); + console.log(` ${dim("Install: https://cli.github.com")}`); + } else if (!isGhAuthenticated()) { + console.log(` ${yellow("!")} GitHub CLI not authenticated`); + console.log(` ${dim("Run: gh auth login")}`); + } else { + console.log(` ${green("✔")} GitHub CLI`); + } + } + + // ── Step 2: QR Authentication ───────────────────────────────── + let address: string | null = null; + if (!opts.skipAuth) { + address = await doQrLogin(); + if (!address) { + process.exitCode = 1; + } + } + + // ── Step 3: Account Status ─────────────────────────────────── + // if (address) { TEMPORARILY DISABLE + if (false) { + console.log(); + await ensureAccountFunded(address); + } + + console.log(); + console.log(`${green("✔")} ${bold("Setup complete")}`); + console.log(); + process.exit(process.exitCode ?? 0); + }); diff --git a/apps/cli/src/commands/remix.ts b/apps/cli/src/commands/remix.ts new file mode 100644 index 0000000..8cdfee1 --- /dev/null +++ b/apps/cli/src/commands/remix.ts @@ -0,0 +1,374 @@ +import { Command } from "commander"; +import { execSync, execFileSync } from "node:child_process"; +import { randomBytes } from "node:crypto"; +import { existsSync, readFileSync, writeFileSync, rmSync } from "node:fs"; +import { resolve, basename } from "node:path"; +import { createInterface } from "node:readline"; +import { connect, fetchIpfs, unwrapOption } from "../connection.js"; +import { type AppMetadata } from "../config.js"; +import { detectPackageManager, ensureToolchain } from "../project.js"; +import { spinner, printTable, truncate, bold, green, dim, cyan, yellow, red } from "../ui.js"; + +/* @integration */ +function ask(prompt: string, fallback?: string): Promise { + const rl = createInterface({ input: process.stdin, output: process.stdout }); + const suffix = fallback ? ` ${dim(`(${fallback})`)}` : ""; + return new Promise((res) => { + rl.question(` ${prompt}${suffix}: `, (answer) => { + rl.close(); + res(answer.trim() || fallback || ""); + }); + }); +} + +function slugify(s: string): string { + return s + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-|-$/g, ""); +} + +function randomSuffix(): string { + return randomBytes(3).toString("hex"); +} + +function stripPostinstall(dir: string) { + const pkgPath = resolve(dir, "package.json"); + if (!existsSync(pkgPath)) return; + try { + const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")); + if (pkg.scripts?.postinstall) { + delete pkg.scripts.postinstall; + writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n"); + } + } catch { + // Malformed package.json — leave as-is + } +} + +// --------------------------------------------------------------------------- +// Interactive app picker (used when no domain arg is provided) +// --------------------------------------------------------------------------- + +/* @integration */ +async function pickApp( + chainName: string, +): Promise<{ domain: string; metadata?: AppMetadata } | null> { + const s = spinner("Browse", "Connecting to registry..."); + let conn; + try { + conn = await connect(chainName); + s.update("Loading apps..."); + + const countRes = await conn.registry.getAppCount.query(); + const total = countRes.success ? Number(countRes.value) : 0; + + if (total === 0) { + s.succeed("No apps in the registry yet."); + return null; + } + + const apps: { domain: string; metadata?: AppMetadata }[] = []; + const gateway = conn.ipfsGateway; + const BATCH = 10; + const LIMIT = 30; + + for (let start = total - 1; start >= 0 && apps.length < LIMIT; start -= BATCH) { + const batchEnd = Math.max(start - BATCH + 1, 0); + const indices = []; + for (let i = start; i >= batchEnd; i--) indices.push(i); + + const domains = await Promise.all( + indices.map(async (idx) => { + const res = await conn!.registry.getDomainAt.query(idx); + if (!res.success) return null; + const domain = unwrapOption(res.value); + return domain ?? null; + }), + ); + + for (const domain of domains) { + if (domain && apps.length < LIMIT) apps.push({ domain }); + } + } + + // Fetch metadata + s.update(`Loading metadata for ${apps.length} apps...`); + await Promise.allSettled( + apps.map(async (m) => { + try { + const res = await conn!.registry.getMetadataUri.query(m.domain); + const cid = unwrapOption(res.success ? res.value : undefined); + if (cid) m.metadata = await fetchIpfs(cid, gateway); + } catch {} + }), + ); + + s.succeed(`${apps.length} apps available`); + console.log(); + + // Display table + const rows = apps.map((m, i) => [ + dim(`${i + 1}`), + bold(m.domain), + m.metadata?.name ?? dim("—"), + truncate(m.metadata?.description ?? "", 40), + ]); + printTable(["#", "Domain", "Name", "Description"], rows); + console.log(); + + // Pick + const choice = await ask("Select an app (number or domain)"); + const num = parseInt(choice, 10); + if (num >= 1 && num <= apps.length) return apps[num - 1]; + const byDomain = apps.find((a) => a.domain === choice || a.domain === `${choice}.dot`); + if (byDomain) return byDomain; + + console.log(` ${dim("Invalid selection.")}`); + return null; + } catch (err) { + s.fail(err instanceof Error ? err.message : String(err)); + return null; + } finally { + conn?.destroy(); + } +} + +// --------------------------------------------------------------------------- +// Clone & setup (shared between interactive and direct modes) +// --------------------------------------------------------------------------- + +/* @integration */ +async function cloneAndSetup( + domain: string, + metadata: AppMetadata, + targetDir: string, + newDomain: string, + newName: string, + opts: { install: boolean }, +) { + const branchLabel = metadata.branch ? ` (${metadata.branch})` : ""; + const s = spinner("Clone", `${metadata.repository}${branchLabel}...`); + const gitArgs = ["clone"]; + if (metadata.branch) gitArgs.push("--branch", metadata.branch); + gitArgs.push(metadata.repository!, targetDir); + execFileSync("git", gitArgs, { stdio: "pipe" }); + + rmSync(`${targetDir}/.git`, { recursive: true, force: true }); + execSync(`git init`, { cwd: targetDir, stdio: "pipe" }); + stripPostinstall(targetDir); + + s.update("Setting up dot.json..."); + const dotJsonPath = resolve(targetDir, "dot.json"); + let dotJson: Record = {}; + if (existsSync(dotJsonPath)) { + try { + dotJson = JSON.parse(readFileSync(dotJsonPath, "utf-8")); + } catch {} + } + dotJson.domain = newDomain; + dotJson.name = newName; + if (!dotJson.description && metadata.description) dotJson.description = metadata.description; + if (!dotJson.tag && metadata.tag) dotJson.tag = metadata.tag; + writeFileSync(dotJsonPath, JSON.stringify(dotJson, null, 2) + "\n"); + + s.succeed(`Remixed → ${bold(targetDir)}`); + + if (opts.install && existsSync(resolve(targetDir, "package.json"))) { + const pm = detectPackageManager(targetDir); + const installSpinner = spinner("Install", `Running ${pm} install...`); + try { + execSync(`${pm} install`, { cwd: targetDir, stdio: "pipe" }); + installSpinner.succeed("Dependencies installed"); + } catch { + installSpinner.fail(`${pm} install failed — run it manually`); + } + } +} + +// --------------------------------------------------------------------------- +// Command +// --------------------------------------------------------------------------- + +/* @integration */ +export const remixCommand = new Command("remix") + .description("Fork an app to customize") + .argument("[domain]", "App domain to remix (e.g. my-app)") + .option("-n, --name ", "Chain to connect to", "paseo") + .option("--quest ", "Run a specific quest (non-interactive)") + .option("--ipfs-gateway-url ", "Override IPFS gateway URL") + .option("--no-install", "Skip dependency installation") + .action(async (rawDomain: string | undefined, opts) => { + // Ensure toolchain (quiet — only prints when installing) + { + let activeSpinner: ReturnType | null = null; + const results = ensureToolchain({ + onStep: (name, status, msg) => { + if (status === "installing") { + activeSpinner = spinner(name, msg ?? `Installing ${name}...`); + } else if (status === "ok" && activeSpinner) { + activeSpinner.succeed(msg ?? name); + activeSpinner = null; + } else if (status === "failed") { + activeSpinner?.fail(msg ?? `Failed to install ${name}`); + activeSpinner = null; + } + }, + }); + const failures = results.filter((r) => !r.ok); + if (failures.length > 0) { + for (const f of failures) { + console.log(` ${red("✖")} ${f.name}: ${f.error}`); + if (f.manualHint) console.log(` ${dim(f.manualHint)}`); + } + process.exit(1); + } + } + + let domain: string; + let metadata: AppMetadata | undefined; + + // ── Resolve app (interactive or direct) ────────────────────── + if (!rawDomain) { + // Interactive mode: browse and pick + const picked = await pickApp(opts.name); + if (!picked) { + process.exit(1); + } + domain = picked.domain; + metadata = picked.metadata; + } else { + domain = rawDomain.endsWith(".dot") ? rawDomain : `${rawDomain}.dot`; + + // Fetch metadata for the specified domain + const s = spinner("Remix", "Connecting..."); + let conn; + try { + conn = await connect(opts.name); + const gateway = opts.ipfsGatewayUrl ?? conn.ipfsGateway; + + s.update(`Looking up ${domain}...`); + const metaRes = await conn.registry.getMetadataUri.query(domain); + const cid = unwrapOption(metaRes.success ? metaRes.value : undefined); + + if (!cid) { + s.fail(`App "${domain}" not found or has no metadata.`); + process.exit(1); + } + + s.update("Fetching metadata..."); + metadata = await fetchIpfs(cid, gateway); + s.succeed(`Found ${bold(domain)}`); + } catch (err) { + s.fail(err instanceof Error ? err.message : String(err)); + process.exit(1); + } finally { + conn?.destroy(); + } + } + + if (!metadata?.repository) { + console.error(` App "${domain}" has no repository URL set. Cannot remix.`); + process.exit(1); + } + + // ── Quest handling (stub) ──────────────────────────────────── + if (opts.quest) { + console.log(` ${yellow("!")} Quest support coming soon.`); + console.log(` ${dim(`Requested quest: ${opts.quest}`)}`); + } + + // ── Clone the app ──────────────────────────────────────────── + const defaultName = metadata.name ?? domain.replace(/\.dot$/, ""); + const newName = await ask("Name for your remix", defaultName); + const newDomain = slugify(newName) + "-" + randomSuffix(); + console.log(` ${dim("→ domain:")} ${bold(newDomain)}`); + + if (existsSync(newDomain)) { + console.error(` Directory "${newDomain}" already exists.`); + process.exit(1); + } + + await cloneAndSetup(domain, metadata, newDomain, newDomain, newName, { + install: opts.install !== false, + }); + + // ── Quest picker stub (after clone) ────────────────────────── + if (!opts.quest) { + console.log(); + console.log(` ${yellow("!")} ${bold("Quests")} — coming soon`); + console.log( + ` ${dim("Interactive quest picker will be available in a future release")}`, + ); + } + + console.log(); + console.log(` ${green("Next steps:")}`); + console.log(` ${dim("1.")} cd ${newDomain}`); + console.log(` ${dim("2.")} edit with claude`); + console.log(` ${dim("3.")} dot deploy --playground`); + console.log(); + process.exit(0); + }); + +if (import.meta.vitest) { + const { test, expect, describe } = import.meta.vitest; + const { + mkdtempSync, + writeFileSync: _writeFile, + readFileSync: _readFile, + rmSync, + } = await import("node:fs"); + const { tmpdir } = await import("node:os"); + const { join } = await import("node:path"); + + describe("slugify", () => { + test("lowercases and replaces spaces", () => { + expect(slugify("Hello World")).toBe("hello-world"); + }); + test("removes special characters", () => { + expect(slugify("my_app@v2!")).toBe("my-app-v2"); + }); + test("trims leading/trailing hyphens", () => { + expect(slugify("--test--")).toBe("test"); + }); + test("collapses multiple separators", () => { + expect(slugify("a b...c")).toBe("a-b-c"); + }); + test("handles empty string", () => { + expect(slugify("")).toBe(""); + }); + }); + + describe("stripPostinstall", () => { + test("removes postinstall from package.json", () => { + const dir = mkdtempSync(join(tmpdir(), "cli-test-")); + _writeFile( + join(dir, "package.json"), + JSON.stringify({ scripts: { postinstall: "echo bad", build: "tsc" } }), + ); + stripPostinstall(dir); + const result = JSON.parse(_readFile(join(dir, "package.json"), "utf-8")); + expect(result.scripts.postinstall).toBeUndefined(); + expect(result.scripts.build).toBe("tsc"); + rmSync(dir, { recursive: true }); + }); + + test("does nothing when no package.json", () => { + const dir = mkdtempSync(join(tmpdir(), "cli-test-")); + stripPostinstall(dir); // should not throw + rmSync(dir, { recursive: true }); + }); + + test("does nothing when no postinstall script", () => { + const dir = mkdtempSync(join(tmpdir(), "cli-test-")); + const pkg = { scripts: { build: "tsc" } }; + _writeFile(join(dir, "package.json"), JSON.stringify(pkg)); + stripPostinstall(dir); + const result = JSON.parse(_readFile(join(dir, "package.json"), "utf-8")); + expect(result.scripts.build).toBe("tsc"); + // File shouldn't be rewritten (no postinstall to remove) + rmSync(dir, { recursive: true }); + }); + }); +} diff --git a/apps/cli/src/commands/update.ts b/apps/cli/src/commands/update.ts new file mode 100644 index 0000000..c34728c --- /dev/null +++ b/apps/cli/src/commands/update.ts @@ -0,0 +1,78 @@ +import { Command } from "commander"; +import { writeFileSync, chmodSync } from "node:fs"; +import { resolve } from "node:path"; +import { homedir, platform, arch } from "node:os"; +import { spinner, bold, dim } from "../ui.js"; +import pkg from "../../package.json" with { type: "json" }; + +function currentVersion(): string { + return pkg.version; +} + +function platformAsset(): string { + const os = platform() === "darwin" ? "darwin" : "linux"; + const cpu = arch() === "arm64" ? "arm64" : "x64"; + return `dot-${os}-${cpu}`; +} + +if (import.meta.vitest) { + const { test, expect, describe } = import.meta.vitest; + + describe("currentVersion", () => { + test("returns a semver string", () => { + expect(currentVersion()).toMatch(/^\d+\.\d+\.\d+$/); + }); + }); + + describe("platformAsset", () => { + test("returns dot-{os}-{arch} format", () => { + const asset = platformAsset(); + expect(asset).toMatch(/^dot-(darwin|linux)-(arm64|x64)$/); + }); + }); +} + +/* @integration */ +export const updateCommand = new Command("update") + .description("Update the dot CLI to the latest version") + .action(async () => { + const s = spinner("Update", "Checking for updates..."); + try { + const repo = "paritytech/polkadot-apps"; + const current = currentVersion(); + + // Fetch latest release tag via redirect header + const res = await fetch(`https://github.com/${repo}/releases/latest`, { + redirect: "manual", + }); + const location = res.headers.get("location"); + if (!location) throw new Error("Could not determine latest release"); + + const latest = location.split("/tag/")[1]; + if (!latest) throw new Error("Could not parse release tag"); + + const latestVersion = latest.replace(/^v/, ""); + + if (latestVersion === current) { + s.succeed(`Already up to date (${bold(current)})`); + return; + } + + s.update(`Downloading ${bold(latest)}...`); + + const asset = platformAsset(); + const url = `https://github.com/${repo}/releases/download/${latest}/${asset}`; + const binRes = await fetch(url); + if (!binRes.ok) throw new Error(`Download failed: ${binRes.statusText}`); + + const binPath = resolve(homedir(), ".polkadot/bin/dot"); + const bytes = new Uint8Array(await binRes.arrayBuffer()); + writeFileSync(binPath, bytes); + chmodSync(binPath, 0o755); + + s.succeed(`Updated ${dim(current)} → ${bold(latestVersion)}`); + } catch (err) { + s.fail(err instanceof Error ? err.message : String(err)); + process.exitCode = 1; + } + }); diff --git a/apps/cli/src/config.ts b/apps/cli/src/config.ts new file mode 100644 index 0000000..d5df228 --- /dev/null +++ b/apps/cli/src/config.ts @@ -0,0 +1,54 @@ +// Chain presets — only ipfsGateway is used here. +// RPC connections go through @polkadot-apps/chain-client presets. +export const CHAINS: Record = { + paseo: { + ipfsGateway: "https://paseo-ipfs.polkadot.io/ipfs", + }, + local: { + ipfsGateway: "http://127.0.0.1:8283/ipfs", + }, +}; + +export const DEFAULT_CHAIN = "paseo"; + +// Metadata schema +export interface AppMetadata { + name?: string; + description?: string; + repository?: string; + branch?: string; + icon_cid?: string; + tag?: string; +} + +// Valid tags +export const TAGS = ["social", "chat", "defi", "utility", "gaming", "marketplace", "irl"] as const; + +if (import.meta.vitest) { + const { test, expect } = import.meta.vitest; + + test("DEFAULT_CHAIN is paseo", () => { + expect(DEFAULT_CHAIN).toBe("paseo"); + }); + + test("CHAINS has expected presets", () => { + expect(Object.keys(CHAINS)).toEqual(expect.arrayContaining(["paseo", "local"])); + }); + + test("each chain has ipfsGateway", () => { + for (const [name, chain] of Object.entries(CHAINS)) { + expect(chain.ipfsGateway, `${name}.ipfsGateway`).toBeTruthy(); + } + }); + + test("local chain uses localhost URL", () => { + expect(CHAINS.local.ipfsGateway).toContain("127.0.0.1"); + }); + + test("TAGS contains expected categories", () => { + expect(TAGS).toContain("social"); + expect(TAGS).toContain("defi"); + expect(TAGS).toContain("gaming"); + expect(TAGS.length).toBe(7); + }); +} diff --git a/apps/cli/src/connection.ts b/apps/cli/src/connection.ts new file mode 100644 index 0000000..fac8457 --- /dev/null +++ b/apps/cli/src/connection.ts @@ -0,0 +1,151 @@ +import { getChainAPI, type Environment } from "@polkadot-apps/chain-client"; +import { createInkSdk } from "@polkadot-api/sdk-ink"; +import { ContractManager } from "@polkadot-apps/contracts"; +import type { SS58String } from "polkadot-api"; +import { CHAINS, DEFAULT_CHAIN } from "./config.js"; +import { getSessionSigner } from "./utils/session.js"; + +// Import as JSON module so bun embeds it in the compiled binary +import cdmJson from "../cdm.json" with { type: "json" }; + +// Map config chain names to chain-client Environment. +// Only paseo is currently available in chain-client presets. +const CHAIN_TO_ENV: Record = { + paseo: "paseo", +}; + +function resolveEnvironment(chainName: string): Environment { + const env = CHAIN_TO_ENV[chainName]; + if (!env) { + const supported = Object.keys(CHAIN_TO_ENV).join(", "); + throw new Error( + `Chain "${chainName}" is not yet supported via chain-client. Supported: ${supported}`, + ); + } + return env; +} + +export interface Connection { + registry: any; + assetHub: any; + ipfsGateway: string; + destroy: () => void; +} + +/* @integration */ +export async function connect(chainName?: string): Promise { + const name = chainName ?? DEFAULT_CHAIN; + const chain = CHAINS[name]; + if (!chain) { + throw new Error(`Unknown chain "${name}". Available: ${Object.keys(CHAINS).join(", ")}`); + } + + const env = resolveEnvironment(name); + const client = await getChainAPI(env); + const inkSdk = createInkSdk(client.raw.assetHub, { atBest: true }); + + // Load persisted QR session so contract queries use the real account as origin + // instead of falling back to Alice with a noisy warning. + const session = await getSessionSigner(); + const manager = new ContractManager(cdmJson, inkSdk, { + ...(session ? { defaultOrigin: session.origin as SS58String } : {}), + }); + const registry = manager.getContract("@example/playground-registry"); + + return { + registry, + assetHub: client.assetHub, + ipfsGateway: chain.ipfsGateway, + // Note: we intentionally skip session?.destroy() here because the + // terminal adapter has a bug where it disconnects the WebSocket before + // unsubscribing statement store listeners, causing DestroyedError noise. + // The process exits shortly after anyway. + destroy: () => client.destroy(), + }; +} + +// IPFS fetch helper +export async function fetchIpfs(cid: string, gatewayUrl: string): Promise { + const sep = gatewayUrl.endsWith("/") ? "" : "/"; + const res = await fetch(`${gatewayUrl}${sep}${cid}`, { signal: AbortSignal.timeout(15_000) }); + if (!res.ok) throw new Error(`IPFS fetch failed: ${res.statusText}`); + return res.json() as Promise; +} + +// Unwrap Substrate Option type +export function unwrapOption(val: unknown): T | undefined { + if (val && typeof val === "object" && "isSome" in val) { + const opt = val as { isSome: boolean; value: T }; + return opt.isSome ? opt.value : undefined; + } + return val as T; +} + +if (import.meta.vitest) { + const { test, expect, describe } = import.meta.vitest; + + describe("unwrapOption", () => { + test("unwraps Some value", () => { + expect(unwrapOption({ isSome: true, value: "hello" })).toBe("hello"); + }); + test("returns undefined for None", () => { + expect(unwrapOption({ isSome: false, value: "" })).toBeUndefined(); + }); + test("passes through non-Option values", () => { + expect(unwrapOption("plain string")).toBe("plain string"); + expect(unwrapOption(42)).toBe(42); + expect(unwrapOption(null)).toBe(null); + }); + test("handles undefined input", () => { + expect(unwrapOption(undefined)).toBeUndefined(); + }); + }); + + describe("resolveEnvironment", () => { + test("resolves paseo", () => { + expect(resolveEnvironment("paseo")).toBe("paseo"); + }); + test("throws for unsupported chain", () => { + expect(() => resolveEnvironment("local")).toThrow("not yet supported"); + }); + test("throws for unknown chain", () => { + expect(() => resolveEnvironment("fakenet")).toThrow("not yet supported"); + }); + test("error message lists supported chains", () => { + expect(() => resolveEnvironment("local")).toThrow("paseo"); + }); + }); + + describe("fetchIpfs", () => { + test("constructs URL with separator", async () => { + const mockFetch = globalThis.fetch; + globalThis.fetch = (async (url: string) => { + expect(url).toBe("https://gateway.io/ipfs/bafk123"); + return { ok: true, json: async () => ({ data: true }) }; + }) as any; + const result = await fetchIpfs("bafk123", "https://gateway.io/ipfs"); + expect(result).toEqual({ data: true }); + globalThis.fetch = mockFetch; + }); + + test("skips separator when gateway ends with /", async () => { + const mockFetch = globalThis.fetch; + globalThis.fetch = (async (url: string) => { + expect(url).toBe("https://gateway.io/ipfs/bafk123"); + return { ok: true, json: async () => ({}) }; + }) as any; + await fetchIpfs("bafk123", "https://gateway.io/ipfs/"); + globalThis.fetch = mockFetch; + }); + + test("throws on non-ok response", async () => { + const mockFetch = globalThis.fetch; + globalThis.fetch = (async () => ({ + ok: false, + statusText: "Not Found", + })) as any; + await expect(fetchIpfs("bafk", "https://gw")).rejects.toThrow("IPFS fetch failed"); + globalThis.fetch = mockFetch; + }); + }); +} diff --git a/apps/cli/src/index.ts b/apps/cli/src/index.ts new file mode 100644 index 0000000..3458403 --- /dev/null +++ b/apps/cli/src/index.ts @@ -0,0 +1,27 @@ +#!/usr/bin/env node + +// WebSocket polyfill — must run before any command imports +import "./polyfill.js"; + +import { Command } from "commander"; +import pkg from "../package.json" with { type: "json" }; +import { initCommand } from "./commands/init.js"; +import { remixCommand } from "./commands/remix.js"; +import { buildCommand } from "./commands/build.js"; +import { deployCommand } from "./commands/deploy.js"; +import { infoCommand } from "./commands/info.js"; +import { updateCommand } from "./commands/update.js"; + +const program = new Command() + .name("dot") + .description("Developer CLI for building Polkadot Apps") + .version(pkg.version); + +program.addCommand(initCommand); +program.addCommand(remixCommand); +program.addCommand(buildCommand); +program.addCommand(deployCommand); +program.addCommand(infoCommand); +program.addCommand(updateCommand); + +program.parse(); diff --git a/apps/cli/src/polyfill.ts b/apps/cli/src/polyfill.ts new file mode 100644 index 0000000..3ee5c9c --- /dev/null +++ b/apps/cli/src/polyfill.ts @@ -0,0 +1,13 @@ +// WebSocket polyfill for Node.js / Bun (required by host-papp SDK). +// Loaded once in index.ts before any command imports. +import { WebSocket as _WS } from "ws"; + +if (!globalThis.WebSocket) { + const WebSocket = new Proxy(_WS, { + construct(target, args) { + const [url, protocols, opts] = args; + return new target(url, protocols, { followRedirects: true, ...opts }); + }, + }); + Object.assign(globalThis, { WebSocket }); +} diff --git a/apps/cli/src/project.ts b/apps/cli/src/project.ts new file mode 100644 index 0000000..ff5d9d1 --- /dev/null +++ b/apps/cli/src/project.ts @@ -0,0 +1,755 @@ +import { existsSync, readFileSync, writeFileSync } from "node:fs"; +import { homedir, platform } from "node:os"; +import { resolve } from "node:path"; +import { execSync } from "node:child_process"; +import { seedToAccount } from "@polkadot-apps/keys"; +import { DEV_PHRASE } from "@polkadot-labs/hdkd-helpers"; + +// --------------------------------------------------------------------------- +// dot.json config +// --------------------------------------------------------------------------- + +export interface ProjectConfig { + domain?: string; + name?: string; + description?: string; + repository?: string; + branch?: string; + tag?: string; + icon?: string; + build?: string; +} + +export function loadProjectConfig(): ProjectConfig { + const dotPath = resolve(process.cwd(), "dot.json"); + if (!existsSync(dotPath)) return {}; + + try { + return JSON.parse(readFileSync(dotPath, "utf-8")); + } catch { + return {}; + } +} + +export function saveDotJson(updates: Partial) { + const dotPath = resolve(process.cwd(), "dot.json"); + let existing: Record = {}; + if (existsSync(dotPath)) { + try { + existing = JSON.parse(readFileSync(dotPath, "utf-8")); + } catch {} + } + const merged = { ...existing }; + for (const [k, v] of Object.entries(updates)) { + if (v !== undefined) merged[k] = v; + } + writeFileSync(dotPath, JSON.stringify(merged, null, 2) + "\n"); +} + +// --------------------------------------------------------------------------- +// Git helpers +// --------------------------------------------------------------------------- + +export function getGitRemoteUrl(): string | undefined { + try { + const url = execSync("git remote get-url origin", { + encoding: "utf-8", + stdio: ["pipe", "pipe", "pipe"], + }).trim(); + return url.replace(/^git@github\.com:/, "https://github.com/").replace(/\.git$/, ""); + } catch { + return undefined; + } +} + +export function getGitBranch(): string | undefined { + try { + return execSync("git symbolic-ref --short HEAD", { + encoding: "utf-8", + stdio: ["pipe", "pipe", "pipe"], + }).trim(); + } catch { + return undefined; + } +} + +// --------------------------------------------------------------------------- +// README +// --------------------------------------------------------------------------- + +const README_NAMES = ["README.md", "readme.md", "README.txt", "README"]; +const MAX_README_SIZE = 512 * 1024; // 512 KB + +export function readReadme(): string | undefined { + for (const name of README_NAMES) { + const path = resolve(process.cwd(), name); + if (existsSync(path)) { + const content = readFileSync(path, "utf-8"); + if (content.length > MAX_README_SIZE) { + return content.slice(0, MAX_README_SIZE); + } + return content; + } + } + return undefined; +} + +// --------------------------------------------------------------------------- +// Project detection helpers (shared by build + deploy commands) +// --------------------------------------------------------------------------- + +export function detectPackageManager(dir: string): "pnpm" | "npm" | "bun" { + if (existsSync(resolve(dir, "pnpm-lock.yaml"))) return "pnpm"; + if (existsSync(resolve(dir, "package-lock.json"))) return "npm"; + if (existsSync(resolve(dir, "bun.lockb")) || existsSync(resolve(dir, "bun.lock"))) return "bun"; + return "pnpm"; +} + +export function hasContracts(): boolean { + return ( + existsSync(resolve(process.cwd(), "contracts")) || + existsSync(resolve(process.cwd(), "Cargo.toml")) + ); +} + +export function getBuildCommand(): string | undefined { + const pkg = resolve(process.cwd(), "package.json"); + try { + const p = JSON.parse(readFileSync(pkg, "utf-8")); + if (p["playground:build"]) return p["playground:build"]; + const pm = detectPackageManager(process.cwd()); + const scripts = p.scripts ?? {}; + if (scripts["build:frontend"]) return `${pm} build:frontend`; + if (scripts.build) return `${pm} build`; + } catch { + // No package.json — no build command + } + return undefined; +} + +// --------------------------------------------------------------------------- +// Toolchain helpers +// --------------------------------------------------------------------------- + +function commandExists(cmd: string): boolean { + try { + execSync(`command -v ${cmd}`, { stdio: "pipe" }); + return true; + } catch { + return false; + } +} + +function hasRustNightly(): boolean { + try { + const out = execSync("rustup toolchain list", { encoding: "utf-8", stdio: "pipe" }); + return out.includes("nightly"); + } catch { + return false; + } +} + +function hasRustSrc(): boolean { + try { + const out = execSync("rustup component list --toolchain nightly", { + encoding: "utf-8", + stdio: "pipe", + }); + return out.includes("rust-src (installed)"); + } catch { + return false; + } +} + +function hasCdm(): boolean { + return commandExists("cdm") && commandExists("cargo-pvm-contract"); +} + +function isIpfsInitialized(): boolean { + return existsSync(resolve(homedir(), ".ipfs")); +} + +function isGhAuthenticated(): boolean { + try { + execSync("gh auth status", { stdio: "pipe" }); + return true; + } catch { + return false; + } +} + +export { commandExists, isGhAuthenticated }; + +interface ToolchainStep { + name: string; + check: () => boolean; + install: () => void; + manualHint?: string; +} + +const TOOLCHAIN_STEPS: ToolchainStep[] = [ + { + name: "rustup", + + check: () => commandExists("rustup"), + install: () => + execSync('curl --proto "=https" --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y', { + stdio: "inherit", + shell: "/bin/bash", + }), + manualHint: "Install manually: https://rustup.rs", + }, + { + name: "Rust nightly", + + check: () => hasRustNightly(), + install: () => execSync("rustup toolchain install nightly", { stdio: "inherit" }), + }, + { + name: "rust-src", + + check: () => hasRustSrc(), + install: () => + execSync("rustup component add rust-src --toolchain nightly", { stdio: "inherit" }), + }, + { + name: "cdm & cargo-pvm-contract", + + check: () => hasCdm(), + install: () => + execSync( + "curl -fsSL https://raw.githubusercontent.com/paritytech/contract-dependency-manager/main/install.sh | bash", + { stdio: "inherit", shell: "/bin/bash" }, + ), + manualHint: + "Install manually: curl -fsSL https://raw.githubusercontent.com/paritytech/contract-dependency-manager/main/install.sh | bash", + }, + { + name: "IPFS", + + check: () => commandExists("ipfs") && isIpfsInitialized(), + install: () => { + if (!commandExists("ipfs")) { + if (platform() === "darwin" && commandExists("brew")) { + execSync("brew install ipfs", { stdio: "inherit" }); + } else if (platform() === "darwin") { + execSync( + "curl -fsSL https://dist.ipfs.tech/kubo/v0.33.2/kubo_v0.33.2_darwin-arm64.tar.gz | tar xz && cd kubo && sudo bash install.sh && cd .. && rm -rf kubo", + { stdio: "inherit", shell: "/bin/bash" }, + ); + } else { + execSync( + "curl -fsSL https://dist.ipfs.tech/kubo/v0.33.2/kubo_v0.33.2_linux-amd64.tar.gz | tar xz && cd kubo && sudo bash install.sh && cd .. && rm -rf kubo", + { stdio: "inherit", shell: "/bin/bash" }, + ); + } + } + if (!isIpfsInitialized()) { + execSync("ipfs init", { stdio: "inherit" }); + } + }, + manualHint: "Install: https://docs.ipfs.tech/install/ then run: ipfs init", + }, +]; + +export interface ToolchainResult { + name: string; + ok: boolean; + installed: boolean; + error?: string; + manualHint?: string; +} + +/** + * Ensure dev toolchain dependencies are installed. + * + * In **quiet** mode (default): prints nothing when everything is already + * installed. Only prints spinner + status when something needs installing. + * + * In **verbose** mode: prints a checkmark for each tool that's already + * present, matching `dot init` output style. + * + * Use `scopes` to only check/install tools needed for the current command. + * Omit scopes (or pass empty) to check everything (used by `dot init`). + * + * @returns Array of results for each toolchain step. + */ +export function ensureToolchain(options?: { + verbose?: boolean; + /** When provided, spinner-style callbacks for install progress. */ + onStep?: ( + name: string, + status: "checking" | "installing" | "ok" | "failed", + msg?: string, + ) => void; +}): ToolchainResult[] { + const verbose = options?.verbose ?? false; + const onStep = options?.onStep; + const results: ToolchainResult[] = []; + + for (const step of TOOLCHAIN_STEPS) { + if (step.check()) { + if (verbose) { + onStep?.(step.name, "ok"); + } + results.push({ name: step.name, ok: true, installed: false }); + continue; + } + + onStep?.(step.name, "installing", `Installing ${step.name}...`); + try { + step.install(); + onStep?.(step.name, "ok", `${step.name} installed`); + results.push({ name: step.name, ok: true, installed: true }); + } catch (err) { + const error = err instanceof Error ? err.message : String(err); + onStep?.(step.name, "failed", error); + results.push({ + name: step.name, + ok: false, + installed: false, + error, + manualHint: step.manualHint, + }); + } + } + + return results; +} + +// --------------------------------------------------------------------------- +// Account & signer +// --------------------------------------------------------------------------- + +export function loadAccount(chain: string): { address: string; mnemonic: string } { + const accountsPath = resolve(homedir(), ".polkadot/accounts.json"); + try { + const accounts = JSON.parse(readFileSync(accountsPath, "utf-8")); + if (!accounts[chain]) { + throw new Error( + `No account found for chain "${chain}". Run "dot init" or use --suri //Alice for dev.`, + ); + } + return accounts[chain]; + } catch (err: unknown) { + if (err instanceof Error && "code" in err && (err as any).code === "ENOENT") { + throw new Error( + `No accounts file found. Run "dot init" or use --suri //Alice for dev.`, + ); + } + throw err; + } +} + +export function prepareSigner(mnemonic: string, derivePath: string = "") { + const account = seedToAccount(mnemonic, derivePath || "//0"); + return { signer: account.signer, origin: account.ss58Address }; +} + +export function resolveSigner(chain: string, suri?: string) { + if (suri) { + if (suri.startsWith("//")) { + return prepareSigner(DEV_PHRASE, suri); + } + throw new Error("Only dev SURIs (//Name) are currently supported. Use --suri //Alice"); + } + const account = loadAccount(chain); + return prepareSigner(account.mnemonic); +} + +export function loadMnemonic(chain: string): string { + return loadAccount(chain).mnemonic; +} + +if (import.meta.vitest) { + const { test, expect, describe, vi } = import.meta.vitest; + const { mkdtempSync, writeFileSync: _writeFile, rmSync } = await import("node:fs"); + const { tmpdir } = await import("node:os"); + const { join } = await import("node:path"); + + // ── loadProjectConfig / saveDotJson ───────────────────────────── + describe("loadProjectConfig", () => { + test("returns empty object when no dot.json", () => { + const dir = mkdtempSync(join(tmpdir(), "cli-test-")); + const orig = process.cwd(); + process.chdir(dir); + expect(loadProjectConfig()).toEqual({}); + process.chdir(orig); + rmSync(dir, { recursive: true }); + }); + + test("returns parsed config from dot.json", () => { + const dir = mkdtempSync(join(tmpdir(), "cli-test-")); + _writeFile(join(dir, "dot.json"), JSON.stringify({ domain: "test" })); + const orig = process.cwd(); + process.chdir(dir); + expect(loadProjectConfig()).toEqual({ domain: "test" }); + process.chdir(orig); + rmSync(dir, { recursive: true }); + }); + + test("returns empty object on invalid JSON", () => { + const dir = mkdtempSync(join(tmpdir(), "cli-test-")); + _writeFile(join(dir, "dot.json"), "not json{{{"); + const orig = process.cwd(); + process.chdir(dir); + expect(loadProjectConfig()).toEqual({}); + process.chdir(orig); + rmSync(dir, { recursive: true }); + }); + }); + + describe("saveDotJson", () => { + test("creates dot.json with updates", () => { + const dir = mkdtempSync(join(tmpdir(), "cli-test-")); + const orig = process.cwd(); + process.chdir(dir); + saveDotJson({ domain: "test", name: "My App" }); + const result = JSON.parse(readFileSync(join(dir, "dot.json"), "utf-8")); + expect(result.domain).toBe("test"); + expect(result.name).toBe("My App"); + process.chdir(orig); + rmSync(dir, { recursive: true }); + }); + + test("merges with existing dot.json", () => { + const dir = mkdtempSync(join(tmpdir(), "cli-test-")); + _writeFile(join(dir, "dot.json"), JSON.stringify({ domain: "old", tag: "defi" })); + const orig = process.cwd(); + process.chdir(dir); + saveDotJson({ domain: "new" }); + const result = JSON.parse(readFileSync(join(dir, "dot.json"), "utf-8")); + expect(result.domain).toBe("new"); + expect(result.tag).toBe("defi"); // preserved + process.chdir(orig); + rmSync(dir, { recursive: true }); + }); + + test("skips undefined values", () => { + const dir = mkdtempSync(join(tmpdir(), "cli-test-")); + const orig = process.cwd(); + process.chdir(dir); + saveDotJson({ domain: "test", name: undefined }); + const result = JSON.parse(readFileSync(join(dir, "dot.json"), "utf-8")); + expect(result.domain).toBe("test"); + expect("name" in result).toBe(false); + process.chdir(orig); + rmSync(dir, { recursive: true }); + }); + }); + + // ── Git URL transformation (tested via real function, works in git repo) ── + describe("getGitRemoteUrl", () => { + test("returns a string or undefined", () => { + const result = getGitRemoteUrl(); + // We're in a git repo so this should return something + expect(result === undefined || typeof result === "string").toBe(true); + }); + + test("converts SSH to HTTPS format", () => { + // Test the transformation logic directly + const transform = (url: string) => + url.replace(/^git@github\.com:/, "https://github.com/").replace(/\.git$/, ""); + expect(transform("git@github.com:paritytech/polkadot-apps.git")).toBe( + "https://github.com/paritytech/polkadot-apps", + ); + expect(transform("https://github.com/paritytech/polkadot-apps.git")).toBe( + "https://github.com/paritytech/polkadot-apps", + ); + expect(transform("https://github.com/paritytech/polkadot-apps")).toBe( + "https://github.com/paritytech/polkadot-apps", + ); + }); + }); + + describe("getGitBranch", () => { + test("returns a string or undefined", () => { + const result = getGitBranch(); + expect(result === undefined || typeof result === "string").toBe(true); + }); + }); + + // ── readReadme (uses temp dir) ──────────────────────────────────── + describe("readReadme", () => { + test("returns undefined in empty directory", () => { + const dir = mkdtempSync(join(tmpdir(), "cli-test-")); + const orig = process.cwd(); + process.chdir(dir); + expect(readReadme()).toBeUndefined(); + process.chdir(orig); + rmSync(dir, { recursive: true }); + }); + + test("reads README.md content", () => { + const dir = mkdtempSync(join(tmpdir(), "cli-test-")); + _writeFile(join(dir, "README.md"), "# Test"); + const orig = process.cwd(); + process.chdir(dir); + expect(readReadme()).toBe("# Test"); + process.chdir(orig); + rmSync(dir, { recursive: true }); + }); + + test("falls back to readme.md", () => { + const dir = mkdtempSync(join(tmpdir(), "cli-test-")); + _writeFile(join(dir, "readme.md"), "# lower"); + const orig = process.cwd(); + process.chdir(dir); + expect(readReadme()).toBe("# lower"); + process.chdir(orig); + rmSync(dir, { recursive: true }); + }); + + test("truncates oversized content", () => { + const dir = mkdtempSync(join(tmpdir(), "cli-test-")); + _writeFile(join(dir, "README.md"), "x".repeat(600_000)); + const orig = process.cwd(); + process.chdir(dir); + expect(readReadme()?.length).toBe(512 * 1024); + process.chdir(orig); + rmSync(dir, { recursive: true }); + }); + }); + + // ── loadAccount (uses temp file) ────────────────────────────────── + describe("loadAccount", () => { + test("returns account when file and chain exist", () => { + const dir = mkdtempSync(join(tmpdir(), "cli-test-")); + const accountsDir = join(dir, ".polkadot"); + const { mkdirSync } = require("node:fs"); + mkdirSync(accountsDir, { recursive: true }); + _writeFile( + join(accountsDir, "accounts.json"), + JSON.stringify({ paseo: { address: "5X", mnemonic: "test words" } }), + ); + // Temporarily override homedir + const origHome = process.env.HOME; + process.env.HOME = dir; + const account = loadAccount("paseo"); + expect(account.address).toBe("5X"); + expect(account.mnemonic).toBe("test words"); + process.env.HOME = origHome; + rmSync(dir, { recursive: true }); + }); + + test("throws when accounts file missing", () => { + expect(() => loadAccount("paseo")).toThrow(/No accounts file|No account/); + }); + + test("throws when chain not in accounts", () => { + const dir = mkdtempSync(join(tmpdir(), "cli-test-")); + const accountsDir = join(dir, ".polkadot"); + const { mkdirSync } = require("node:fs"); + mkdirSync(accountsDir, { recursive: true }); + _writeFile(join(accountsDir, "accounts.json"), JSON.stringify({ polkadot: {} })); + const origHome = process.env.HOME; + process.env.HOME = dir; + expect(() => loadAccount("paseo")).toThrow("No account found"); + process.env.HOME = origHome; + rmSync(dir, { recursive: true }); + }); + }); + + describe("loadMnemonic", () => { + test("returns mnemonic from loadAccount", () => { + const dir = mkdtempSync(join(tmpdir(), "cli-test-")); + const accountsDir = join(dir, ".polkadot"); + const { mkdirSync } = require("node:fs"); + mkdirSync(accountsDir, { recursive: true }); + _writeFile( + join(accountsDir, "accounts.json"), + JSON.stringify({ paseo: { address: "5X", mnemonic: "secret phrase" } }), + ); + const origHome = process.env.HOME; + process.env.HOME = dir; + expect(loadMnemonic("paseo")).toBe("secret phrase"); + process.env.HOME = origHome; + rmSync(dir, { recursive: true }); + }); + }); + + // ── resolveSigner ───────────────────────────────────────────────── + describe("resolveSigner", () => { + test("uses DEV_PHRASE for //Alice", () => { + const result = resolveSigner("paseo", "//Alice"); + expect(result.signer).toBeDefined(); + expect(result.signer.publicKey).toBeInstanceOf(Uint8Array); + expect(result.origin).toMatch(/^5/); + }); + + test("uses DEV_PHRASE for //Bob", () => { + const alice = resolveSigner("paseo", "//Alice"); + const bob = resolveSigner("paseo", "//Bob"); + expect(alice.origin).not.toBe(bob.origin); + }); + + test("throws for non-dev SURIs", () => { + expect(() => resolveSigner("paseo", "0xdeadbeef")).toThrow("Only dev SURIs"); + }); + }); + + // ── detectPackageManager ──────────────────────────────────────────── + describe("detectPackageManager", () => { + test("detects pnpm from lockfile", () => { + const dir = mkdtempSync(join(tmpdir(), "cli-test-")); + _writeFile(join(dir, "pnpm-lock.yaml"), ""); + expect(detectPackageManager(dir)).toBe("pnpm"); + rmSync(dir, { recursive: true }); + }); + + test("detects npm from package-lock.json", () => { + const dir = mkdtempSync(join(tmpdir(), "cli-test-")); + _writeFile(join(dir, "package-lock.json"), "{}"); + expect(detectPackageManager(dir)).toBe("npm"); + rmSync(dir, { recursive: true }); + }); + + test("detects bun from bun.lock", () => { + const dir = mkdtempSync(join(tmpdir(), "cli-test-")); + _writeFile(join(dir, "bun.lock"), ""); + expect(detectPackageManager(dir)).toBe("bun"); + rmSync(dir, { recursive: true }); + }); + + test("defaults to pnpm when no lockfile", () => { + const dir = mkdtempSync(join(tmpdir(), "cli-test-")); + expect(detectPackageManager(dir)).toBe("pnpm"); + rmSync(dir, { recursive: true }); + }); + }); + + // ── hasContracts ───────────────────────────────────────────────── + describe("hasContracts", () => { + test("returns false in empty directory", () => { + const dir = mkdtempSync(join(tmpdir(), "cli-test-")); + const orig = process.cwd(); + process.chdir(dir); + expect(hasContracts()).toBe(false); + process.chdir(orig); + rmSync(dir, { recursive: true }); + }); + + test("returns true when contracts/ exists", () => { + const dir = mkdtempSync(join(tmpdir(), "cli-test-")); + const { mkdirSync } = require("node:fs"); + mkdirSync(join(dir, "contracts")); + const orig = process.cwd(); + process.chdir(dir); + expect(hasContracts()).toBe(true); + process.chdir(orig); + rmSync(dir, { recursive: true }); + }); + + test("returns true when Cargo.toml exists", () => { + const dir = mkdtempSync(join(tmpdir(), "cli-test-")); + _writeFile(join(dir, "Cargo.toml"), "[package]"); + const orig = process.cwd(); + process.chdir(dir); + expect(hasContracts()).toBe(true); + process.chdir(orig); + rmSync(dir, { recursive: true }); + }); + }); + + // ── getBuildCommand ────────────────────────────────────────────── + describe("getBuildCommand", () => { + test("returns undefined when no package.json", () => { + const dir = mkdtempSync(join(tmpdir(), "cli-test-")); + const orig = process.cwd(); + process.chdir(dir); + expect(getBuildCommand()).toBeUndefined(); + process.chdir(orig); + rmSync(dir, { recursive: true }); + }); + + test("returns build:frontend if present", () => { + const dir = mkdtempSync(join(tmpdir(), "cli-test-")); + _writeFile( + join(dir, "package.json"), + JSON.stringify({ scripts: { "build:frontend": "vite build", build: "tsc" } }), + ); + const orig = process.cwd(); + process.chdir(dir); + expect(getBuildCommand()).toBe("pnpm build:frontend"); + process.chdir(orig); + rmSync(dir, { recursive: true }); + }); + + test("falls back to build script", () => { + const dir = mkdtempSync(join(tmpdir(), "cli-test-")); + _writeFile(join(dir, "package.json"), JSON.stringify({ scripts: { build: "tsc" } })); + const orig = process.cwd(); + process.chdir(dir); + expect(getBuildCommand()).toBe("pnpm build"); + process.chdir(orig); + rmSync(dir, { recursive: true }); + }); + + test("returns undefined when no build scripts", () => { + const dir = mkdtempSync(join(tmpdir(), "cli-test-")); + _writeFile(join(dir, "package.json"), JSON.stringify({ scripts: { test: "vitest" } })); + const orig = process.cwd(); + process.chdir(dir); + expect(getBuildCommand()).toBeUndefined(); + process.chdir(orig); + rmSync(dir, { recursive: true }); + }); + + test("uses bun prefix when bun.lock present", () => { + const dir = mkdtempSync(join(tmpdir(), "cli-test-")); + _writeFile(join(dir, "bun.lock"), ""); + _writeFile(join(dir, "package.json"), JSON.stringify({ scripts: { build: "vite" } })); + const orig = process.cwd(); + process.chdir(dir); + expect(getBuildCommand()).toBe("bun build"); + process.chdir(orig); + rmSync(dir, { recursive: true }); + }); + + test("prefers playground:build over scripts", () => { + const dir = mkdtempSync(join(tmpdir(), "cli-test-")); + _writeFile( + join(dir, "package.json"), + JSON.stringify({ + "playground:build": "custom-build-cmd", + scripts: { build: "tsc" }, + }), + ); + const orig = process.cwd(); + process.chdir(dir); + expect(getBuildCommand()).toBe("custom-build-cmd"); + process.chdir(orig); + rmSync(dir, { recursive: true }); + }); + }); + + // ── prepareSigner ───────────────────────────────────────────────── + describe("prepareSigner", () => { + const mnemonic = "bottom drive obey lake curtain smoke basket hold race lonely fit walk"; + + test("returns signer and SS58 origin", () => { + const result = prepareSigner(mnemonic, "//Alice"); + expect(result.signer).toBeDefined(); + expect(result.signer.publicKey).toBeInstanceOf(Uint8Array); + expect(result.origin).toMatch(/^5/); + }); + + test("is deterministic", () => { + const a = prepareSigner(mnemonic, "//Alice"); + const b = prepareSigner(mnemonic, "//Alice"); + expect(a.origin).toBe(b.origin); + }); + + test("different paths produce different signers", () => { + const a = prepareSigner(mnemonic, "//Alice"); + const b = prepareSigner(mnemonic, "//Bob"); + expect(a.origin).not.toBe(b.origin); + }); + + test("uses //0 when derivePath is empty", () => { + const a = prepareSigner(mnemonic, ""); + const b = prepareSigner(mnemonic, "//0"); + expect(a.origin).toBe(b.origin); + }); + }); +} diff --git a/apps/cli/src/ui.ts b/apps/cli/src/ui.ts new file mode 100644 index 0000000..6fdc1c4 --- /dev/null +++ b/apps/cli/src/ui.ts @@ -0,0 +1,224 @@ +// Terminal formatting helpers + +export const bold = (s: string) => `\x1b[1m${s}\x1b[0m`; +export const dim = (s: string) => `\x1b[2m${s}\x1b[0m`; +export const green = (s: string) => `\x1b[32m${s}\x1b[0m`; +export const red = (s: string) => `\x1b[31m${s}\x1b[0m`; +export const cyan = (s: string) => `\x1b[36m${s}\x1b[0m`; +export const yellow = (s: string) => `\x1b[33m${s}\x1b[0m`; +export const magenta = (s: string) => `\x1b[35m${s}\x1b[0m`; + +// Spinner for async operations +export function spinner(label: string, detail: string) { + let i = 0; + const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; + const id = setInterval(() => { + process.stdout.write(`\r\x1b[2K${bold(label)} ${frames[i++ % frames.length]} ${detail}`); + }, 80); + const handle = { + done: false, + update(newDetail: string) { + detail = newDetail; + }, + succeed(msg?: string) { + clearInterval(id); + handle.done = true; + process.stdout.write(`\r\x1b[2K${bold(label)} ${green("✔")} ${msg ?? detail}\n`); + }, + fail(msg?: string) { + clearInterval(id); + handle.done = true; + process.stdout.write(`\r\x1b[2K${bold(label)} ${red("✖")} ${msg ?? detail}\n`); + }, + }; + return handle; +} + +// Strip ANSI escape sequences for visible-length measurement +const stripAnsi = (s: string) => s.replace(/\x1b\[[0-9;]*m/g, ""); + +// Pad string to visible width, accounting for ANSI codes +function padEndVisible(s: string, width: number): string { + const visible = stripAnsi(s).length; + return s + " ".repeat(Math.max(0, width - visible)); +} + +// Table formatting +export function printTable(headers: string[], rows: string[][]) { + const colWidths = headers.map((h, i) => + Math.max(stripAnsi(h).length, ...rows.map((r) => stripAnsi(r[i] ?? "").length)), + ); + + const sep = colWidths.map((w) => "─".repeat(w + 2)).join("┼"); + const fmtRow = (row: string[]) => + row.map((cell, i) => ` ${padEndVisible(cell ?? "", colWidths[i])} `).join("│"); + + console.log(dim(fmtRow(headers))); + console.log(dim(sep)); + for (const row of rows) { + console.log(fmtRow(row)); + } +} + +// Truncate string +export function truncate(s: string, max: number): string { + return s.length > max ? s.slice(0, max - 1) + "…" : s; +} + +if (import.meta.vitest) { + const { test, expect, describe, vi, beforeEach } = import.meta.vitest; + + // ── truncate ────────────────────────────────────────────────────── + describe("truncate", () => { + test("returns string unchanged when shorter than max", () => { + expect(truncate("hello", 10)).toBe("hello"); + }); + test("returns string unchanged when equal to max", () => { + expect(truncate("hello", 5)).toBe("hello"); + }); + test("truncates with ellipsis when longer than max", () => { + expect(truncate("hello world", 8)).toBe("hello w…"); + }); + test("handles empty string", () => { + expect(truncate("", 5)).toBe(""); + }); + test("handles max of 1", () => { + expect(truncate("hello", 1)).toBe("…"); + }); + }); + + // ── ANSI helpers ────────────────────────────────────────────────── + describe("color functions", () => { + test("bold wraps with ANSI codes", () => { + expect(bold("hi")).toBe("\x1b[1mhi\x1b[0m"); + }); + test("green wraps with color code", () => { + expect(green("ok")).toBe("\x1b[32mok\x1b[0m"); + }); + test("red wraps with color code", () => { + expect(red("err")).toBe("\x1b[31merr\x1b[0m"); + }); + }); + + describe("stripAnsi", () => { + test("removes ANSI escape sequences", () => { + expect(stripAnsi(bold("hello"))).toBe("hello"); + }); + test("removes multiple color codes", () => { + expect(stripAnsi(`${green("a")} ${red("b")}`)).toBe("a b"); + }); + test("returns plain string unchanged", () => { + expect(stripAnsi("plain")).toBe("plain"); + }); + test("handles empty string", () => { + expect(stripAnsi("")).toBe(""); + }); + }); + + describe("padEndVisible", () => { + test("pads plain string to width", () => { + expect(padEndVisible("hi", 5)).toBe("hi "); + }); + test("pads colored string to visible width", () => { + const colored = green("hi"); + const padded = padEndVisible(colored, 5); + expect(stripAnsi(padded)).toBe("hi "); + expect(padded).toContain("\x1b[32m"); // still has color + }); + test("does not pad when already at width", () => { + expect(padEndVisible("hello", 5)).toBe("hello"); + }); + test("does not truncate when over width", () => { + expect(padEndVisible("hello!", 3)).toBe("hello!"); + }); + }); + + // ── printTable ──────────────────────────────────────────────────── + describe("printTable", () => { + test("outputs formatted table", () => { + const logs: string[] = []; + vi.spyOn(console, "log").mockImplementation((s: string) => logs.push(s)); + printTable( + ["A", "B"], + [ + ["1", "22"], + ["333", "4"], + ], + ); + vi.restoreAllMocks(); + + expect(logs.length).toBe(4); // header + separator + 2 rows + // Check separator uses ─ and ┼ + expect(stripAnsi(logs[1])).toMatch(/─+┼─+/); + // Check alignment: column widths should accommodate "333" and "22" + const headerRow = stripAnsi(logs[0]); + const dataRow = stripAnsi(logs[2]); + expect(headerRow.length).toBe(dataRow.length); + }); + + test("handles empty rows", () => { + const logs: string[] = []; + vi.spyOn(console, "log").mockImplementation((s: string) => logs.push(s)); + printTable(["X"], []); + vi.restoreAllMocks(); + + expect(logs.length).toBe(2); // header + separator only + }); + }); + + // ── spinner ─────────────────────────────────────────────────────── + describe("spinner", () => { + beforeEach(() => { + vi.useFakeTimers(); + vi.spyOn(process.stdout, "write").mockImplementation(() => true); + }); + + test("succeed sets done to true and writes checkmark", () => { + const s = spinner("Test", "working..."); + expect(s.done).toBe(false); + s.succeed("done!"); + expect(s.done).toBe(true); + const calls = vi.mocked(process.stdout.write).mock.calls; + const last = calls[calls.length - 1][0] as string; + expect(last).toContain("✔"); + expect(last).toContain("done!"); + vi.useRealTimers(); + vi.restoreAllMocks(); + }); + + test("fail sets done to true and writes X", () => { + const s = spinner("Test", "working..."); + s.fail("oops"); + expect(s.done).toBe(true); + const calls = vi.mocked(process.stdout.write).mock.calls; + const last = calls[calls.length - 1][0] as string; + expect(last).toContain("✖"); + expect(last).toContain("oops"); + vi.useRealTimers(); + vi.restoreAllMocks(); + }); + + test("update changes the detail text", () => { + const s = spinner("Test", "old"); + s.update("new"); + vi.advanceTimersByTime(100); + const calls = vi.mocked(process.stdout.write).mock.calls; + const last = calls[calls.length - 1][0] as string; + expect(last).toContain("new"); + expect(last).not.toContain("old"); + s.succeed(); + vi.useRealTimers(); + vi.restoreAllMocks(); + }); + + test("succeed uses detail as default message", () => { + const s = spinner("Test", "status"); + s.succeed(); + const calls = vi.mocked(process.stdout.write).mock.calls; + const last = calls[calls.length - 1][0] as string; + expect(last).toContain("status"); + vi.useRealTimers(); + vi.restoreAllMocks(); + }); + }); +} diff --git a/apps/cli/src/utils/account-handler.ts b/apps/cli/src/utils/account-handler.ts new file mode 100644 index 0000000..1278ec3 --- /dev/null +++ b/apps/cli/src/utils/account-handler.ts @@ -0,0 +1,109 @@ +import type { ChainClient, PresetChains } from "@polkadot-apps/chain-client"; +import type { AccountBalance } from "@polkadot-apps/utils"; +import type { AuthorizationStatus } from "@polkadot-apps/bulletin"; +import { createInkSdk } from "@polkadot-api/sdk-ink"; +import { Enum } from "polkadot-api"; +import { DEV_PHRASE } from "@polkadot-labs/hdkd-helpers"; +import { submitAndWatch, ensureAccountMapped } from "@polkadot-apps/tx"; +import { prepareSigner } from "../project.js"; +import { getSessionSigner } from "./session.js"; + +type PaseoClient = ChainClient>; + +const MIN_BALANCE = 10_000_000_000n; // 1 PAS +export const FUND_AMOUNT = 100_000_000_000n; // 10 PAS +export const BULLETIN_TRANSACTIONS = 1000; +export const BULLETIN_BYTES = 100_000_000n; // 100 MB + +export interface AccountStatus { + balance: AccountBalance; + mapped: boolean; + auth: AuthorizationStatus; +} + +/** + * Fetch the account's on-chain status: Asset Hub balance, Revive mapping, and Bulletin allowance. + * All queries use best-block to stay consistent with transactions that resolve at best-block. + */ +export async function fetchAccountStatus( + client: PaseoClient, + address: string, +): Promise { + const AT_BEST = { at: "best" as const }; + + // Balance at best-block + const account = await client.assetHub.query.System.Account.getValue(address, AT_BEST); + const balance: AccountBalance = { + free: account.data.free, + reserved: account.data.reserved, + frozen: account.data.frozen, + }; + + // Mapping check — inkSdk created with atBest: true already queries best-block + const inkSdk = createInkSdk(client.raw.assetHub, { atBest: true }); + const mapped = await inkSdk.addressIsMapped(address); + + // Bulletin allowance at best-block + const authRaw = await client.bulletin.query.TransactionStorage.Authorizations.getValue( + Enum("Account", address), + AT_BEST, + ); + const auth: AuthorizationStatus = authRaw + ? { + authorized: true, + remainingTransactions: authRaw.extent.transactions, + remainingBytes: authRaw.extent.bytes, + expiration: authRaw.expiration, + } + : { authorized: false, remainingTransactions: 0, remainingBytes: 0n, expiration: 0 }; + + return { balance, mapped, auth }; +} + +/** + * Transfer PAS from Alice to the given address on Asset Hub. + * Returns the new balance after funding. + */ +export async function fundFromAlice(client: PaseoClient, address: string): Promise { + const alice = prepareSigner(DEV_PHRASE, "//Alice"); + await submitAndWatch( + client.assetHub.tx.Balances.transfer_keep_alive({ + dest: Enum("Id", address), + value: FUND_AMOUNT, + }), + alice.signer, + ); +} + +/** + * Map the account for the Revive pallet. + * Uses the provided signer if given, otherwise falls back to the QR session signer. + */ +export async function mapAccount(client: PaseoClient, address: string): Promise { + const session = await getSessionSigner(); + if (!session) { + throw new Error("No session available for signing"); + } + const inkSdk = createInkSdk(client.raw.assetHub, { atBest: true }); + await ensureAccountMapped(address, session.signer, inkSdk, client.assetHub); +} + +/** + * Grant Bulletin storage allowance from Alice. + */ +export async function grantBulletinAllowance(client: PaseoClient, address: string): Promise { + const alice = prepareSigner(DEV_PHRASE, "//Alice"); + await submitAndWatch( + client.bulletin.tx.TransactionStorage.authorize_account({ + who: address, + transactions: BULLETIN_TRANSACTIONS, + bytes: BULLETIN_BYTES, + }), + alice.signer, + ); +} + +/** Whether the account needs funding. */ +export function needsFunding(balance: AccountBalance): boolean { + return balance.free < MIN_BALANCE; +} diff --git a/apps/cli/src/utils/session.ts b/apps/cli/src/utils/session.ts new file mode 100644 index 0000000..dd092e2 --- /dev/null +++ b/apps/cli/src/utils/session.ts @@ -0,0 +1,58 @@ +import type { PolkadotSigner } from "polkadot-api"; + +/** + * Load a persisted QR session from ~/.polkadot-apps/ and return its signer + origin. + * Returns null if no session is available (e.g. user never ran `dot init`). + */ +export async function getSessionSigner(): Promise<{ + signer: PolkadotSigner; + origin: string; + destroy: () => void; +} | null> { + try { + const { + createTerminalAdapter, + createSessionSigner, + DEFAULT_METADATA_URL, + DEFAULT_PEOPLE_ENDPOINTS, + } = await import("@polkadot-apps/terminal"); + + const adapter = createTerminalAdapter({ + appId: "dot-cli", + metadataUrl: DEFAULT_METADATA_URL, + endpoints: DEFAULT_PEOPLE_ENDPOINTS, + }); + + const session = await new Promise((resolve) => { + let resolved = false; + let unsub: (() => void) | null = null; + unsub = adapter.sessions.sessions.subscribe((sessions: any[]) => { + if (sessions.length > 0 && !resolved) { + resolved = true; + queueMicrotask(() => unsub?.()); + resolve(sessions[0]); + } + }); + setTimeout(() => { + if (!resolved) { + resolved = true; + unsub?.(); + resolve(null); + } + }, 3000); + }); + + if (!session) { + adapter.destroy(); + return null; + } + + const { ss58Address } = await import("@polkadot-labs/hdkd-helpers"); + const signer = createSessionSigner(session); + const origin = ss58Address(new Uint8Array(session.remoteAccount.accountId)); + + return { signer, origin, destroy: () => adapter.destroy() }; + } catch { + return null; + } +} diff --git a/apps/cli/tsconfig.json b/apps/cli/tsconfig.json new file mode 100644 index 0000000..136e361 --- /dev/null +++ b/apps/cli/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*", "./.cdm/**/*"] +} diff --git a/biome.json b/biome.json index 97a8ac0..7714784 100644 --- a/biome.json +++ b/biome.json @@ -1,7 +1,14 @@ { "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", "files": { - "include": ["packages/**/*.ts", "packages/**/*.tsx", "packages/**/*.json"], + "include": [ + "packages/**/*.ts", + "packages/**/*.tsx", + "packages/**/*.json", + "apps/**/*.ts", + "apps/**/*.tsx", + "apps/**/*.json" + ], "ignore": [ "**/node_modules/**", "**/dist/**", diff --git a/examples/terminal-login/index.ts b/examples/terminal-login/index.ts index 188e633..95952ca 100644 --- a/examples/terminal-login/index.ts +++ b/examples/terminal-login/index.ts @@ -13,14 +13,12 @@ import { createTerminalAdapter, renderQrCode, waitForSessions, + DEFAULT_METADATA_URL, type PappAdapter, type PairingStatus, type AttestationStatus, } from "@polkadot-apps/terminal"; -const DEFAULT_METADATA_URL = - "https://gist.githubusercontent.com/ReinhardHatko/27415c91178d74196d7c1116d39056d5/raw/56e61d719251170828a80f12d34343a8617b9935/metadata.json"; - // ─── Helpers ───────────────────────────────────────────────────────────────── function prompt(question: string): Promise { diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..7b69638 --- /dev/null +++ b/install.sh @@ -0,0 +1,70 @@ +#!/bin/bash +set -e + +DOT_DIR="$HOME/.polkadot" +REPO="paritytech/polkadot-apps" +BIN="dot" + +# 1) Detect platform +OS=$(uname -s); case "$OS" in Linux) OS=linux;; Darwin) OS=darwin;; *) echo "Unsupported OS: $OS"; exit 1;; esac +ARCH=$(uname -m); case "$ARCH" in x86_64|amd64) ARCH=x64;; arm64|aarch64) ARCH=arm64;; *) echo "Unsupported arch: $ARCH"; exit 1;; esac +ASSET="$BIN-$OS-$ARCH" + +# 2) Resolve release tag +if [ -n "$DOT_TAG" ]; then + TAG="$DOT_TAG" +else + # Try latest stable release first + TAG=$(curl -fsSI "https://github.com/$REPO/releases/latest" \ + | sed -n 's|^location:.*/tag/\(.*\)$|\1|p' | tr -d '\r' | head -n1) || true + # Fall back to newest release of any kind (including pre-releases) + if [ -z "$TAG" ]; then + TAG=$(curl -fsSL "https://api.github.com/repos/$REPO/releases?per_page=1" \ + | sed -n 's/.*"tag_name": *"\([^"]*\)".*/\1/p' | head -n1) || true + fi +fi +[ -z "$TAG" ] && echo "Could not determine latest release" && exit 1 + +# 3) Install binary +mkdir -p "$DOT_DIR/bin" "$HOME/.local/bin" +curl -fsSL "https://github.com/$REPO/releases/download/$TAG/$ASSET" -o "$DOT_DIR/bin/$BIN" +chmod +x "$DOT_DIR/bin/$BIN" +if [ "$OS" = "darwin" ]; then + # Ad-hoc sign — Apple Silicon requires at least this to run a binary + codesign --sign - --force "$DOT_DIR/bin/$BIN" 2>/dev/null || true + # Strip quarantine/provenance xattrs + xattr -c "$DOT_DIR/bin/$BIN" 2>/dev/null || true +fi +ln -sf "$DOT_DIR/bin/$BIN" "$HOME/.local/bin/$BIN" + +echo "Installed $BIN ($OS/$ARCH) from $TAG -> $DOT_DIR/bin/$BIN" + +# 4) Add to PATH in all available shell profiles +append_once() { + local file="$1" line="$2" + grep -Fqx "$line" "$file" 2>/dev/null || printf "\n%s\n" "$line" >> "$file" +} + +if command -v bash >/dev/null 2>&1; then + append_once "$HOME/.bashrc" 'export PATH="$HOME/.polkadot/bin:$HOME/.local/bin:$PATH"' + append_once "$HOME/.bash_profile" '[ -f "$HOME/.bashrc" ] && . "$HOME/.bashrc"' + echo "bash PATH configured" +fi + +if command -v zsh >/dev/null 2>&1; then + append_once "$HOME/.zshrc" 'export PATH="$HOME/.polkadot/bin:$HOME/.local/bin:$PATH"' + echo "zsh PATH configured" +fi + +if command -v fish >/dev/null 2>&1; then + mkdir -p "$HOME/.config/fish" + append_once "$HOME/.config/fish/config.fish" 'fish_add_path $HOME/.polkadot/bin $HOME/.local/bin' + echo "fish PATH configured" +fi + +export PATH="$DOT_DIR/bin:$HOME/.local/bin:$PATH" + +echo "" +echo -e "dot is ready! Running: \033[1mdot init\033[0m" +echo "" +"$DOT_DIR/bin/$BIN" init diff --git a/package.json b/package.json index cab5527..a271730 100644 --- a/package.json +++ b/package.json @@ -13,15 +13,16 @@ "format": "biome format --write .", "format:check": "biome format .", "generate-descriptors": "bash packages/descriptors/scripts/generate.sh", - "docs": "typedoc" + "docs": "typedoc", + "cli:install": "pnpm build && bun build --compile ./apps/cli/src/index.ts --outfile ~/.polkadot/bin/dot && bash ./scripts/cli-path.sh" }, "devDependencies": { "@biomejs/biome": "catalog:", "@changesets/cli": "^2.29.8", + "@vitest/coverage-istanbul": "catalog:", "turbo": "catalog:", "typedoc": "catalog:", "typescript": "catalog:", - "@vitest/coverage-istanbul": "catalog:", "vitest": "catalog:" }, "pnpm": { diff --git a/packages/contracts/src/wrap.ts b/packages/contracts/src/wrap.ts index 6cf841b..3b9c122 100644 --- a/packages/contracts/src/wrap.ts +++ b/packages/contracts/src/wrap.ts @@ -127,12 +127,27 @@ export function wrapContract( ); const data = positionalToNamed(argNames, positionalArgs); const origin = resolveOrigin(defaults, overrides?.origin, true)!; - - const result = await inkContract.query(methodName, { - origin, + const queryOpts = { data, ...(overrides?.value !== undefined && { value: overrides.value }), + }; + + let result = await inkContract.query(methodName, { + origin, + ...queryOpts, }); + + // If the query failed and we used a signer-provided origin, + // retry with the dev fallback. The signer's account may not + // be mapped for the Revive pallet, which causes dry-runs to + // fail even for read-only view calls. + if (!result.success && origin !== QUERY_FALLBACK_ORIGIN && !overrides?.origin) { + result = await inkContract.query(methodName, { + origin: QUERY_FALLBACK_ORIGIN, + ...queryOpts, + }); + } + return { success: result.success, value: result.success ? result.value.response : undefined, diff --git a/packages/terminal/scripts/patch-wasm.sh b/packages/terminal/scripts/patch-wasm.sh index 70d68ff..e72cc07 100755 --- a/packages/terminal/scripts/patch-wasm.sh +++ b/packages/terminal/scripts/patch-wasm.sh @@ -10,7 +10,8 @@ BUNDLER_JS=$(find node_modules -path "*/verifiablejs/pkg-bundler/verifiablejs.js head -1 "$BUNDLER_JS" | grep -q "__wbg_set_wasm" && exit 0 BUNDLER_DIR=$(dirname "$BUNDLER_JS") -WASM_B64=$(base64 -i "$BUNDLER_DIR/verifiablejs_bg.wasm" 2>/dev/null || base64 "$BUNDLER_DIR/verifiablejs_bg.wasm") +# base64 flags differ: macOS uses -i, Linux uses -w 0 to suppress line wrapping +WASM_B64=$(base64 -w 0 "$BUNDLER_DIR/verifiablejs_bg.wasm" 2>/dev/null || base64 -i "$BUNDLER_DIR/verifiablejs_bg.wasm" 2>/dev/null || base64 "$BUNDLER_DIR/verifiablejs_bg.wasm" | tr -d '\n') cat > "$BUNDLER_JS" << SHIM import { __wbg_set_wasm } from "./verifiablejs_bg.js"; diff --git a/packages/terminal/src/adapter.ts b/packages/terminal/src/adapter.ts index f181038..bb14017 100644 --- a/packages/terminal/src/adapter.ts +++ b/packages/terminal/src/adapter.ts @@ -29,6 +29,13 @@ export interface TerminalAdapterOptions { hostMetadata?: HostMetadata; } +/** Default metadata URL for the `dot` CLI app pairing screen. */ +export const DEFAULT_METADATA_URL = + "https://gist.githubusercontent.com/ReinhardHatko/1967dd3f4afe78683cc0ba14d6ec8744/raw/c1625eb7ed7671b7e09a3fa2a25998dde33c70b8/metadata.json"; + +/** Default People chain endpoints for SSO attestation. */ +export const DEFAULT_PEOPLE_ENDPOINTS = ["wss://paseo-people-next-rpc.polkadot.io"]; + /** * Create a terminal adapter backed by the host-papp SDK. * diff --git a/packages/terminal/src/index.ts b/packages/terminal/src/index.ts index 7851275..12c6580 100644 --- a/packages/terminal/src/index.ts +++ b/packages/terminal/src/index.ts @@ -1,6 +1,8 @@ // Terminal Adapter export { createTerminalAdapter, + DEFAULT_METADATA_URL, + DEFAULT_PEOPLE_ENDPOINTS, SS_STABLE_STAGE_ENDPOINTS, SS_PASEO_STABLE_STAGE_ENDPOINTS, } from "./adapter.js"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fa32386..5ca4818 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -45,6 +45,12 @@ catalogs: '@vitest/coverage-istanbul': specifier: ^3.1.4 version: 3.2.4 + bulletin-deploy: + specifier: ^0.6.4 + version: 0.6.4 + commander: + specifier: ^12.0.0 + version: 12.1.0 dexie: specifier: ^4.0.11 version: 4.4.2 @@ -69,6 +75,9 @@ catalogs: vitest: specifier: ^3.1.4 version: 3.2.4 + ws: + specifier: ^8.18.0 + version: 8.20.0 overrides: picomatch@2: 2.3.2 @@ -103,6 +112,55 @@ importers: specifier: 'catalog:' version: 3.2.4(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3) + apps/cli: + dependencies: + '@polkadot-api/sdk-ink': + specifier: 'catalog:' + version: 0.6.2(@polkadot-api/ink-contracts@0.6.0)(polkadot-api@1.23.3(jiti@2.6.1)(postcss@8.5.9)(rxjs@7.8.2)(tsx@4.21.0)(yaml@2.8.3))(rxjs@7.8.2)(typescript@5.9.3) + '@polkadot-apps/address': + specifier: workspace:* + version: link:../../packages/address + '@polkadot-apps/bulletin': + specifier: workspace:* + version: link:../../packages/bulletin + '@polkadot-apps/chain-client': + specifier: workspace:* + version: link:../../packages/chain-client + '@polkadot-apps/contracts': + specifier: workspace:* + version: link:../../packages/contracts + '@polkadot-apps/keys': + specifier: workspace:* + version: link:../../packages/keys + '@polkadot-apps/terminal': + specifier: workspace:* + version: link:../../packages/terminal + '@polkadot-apps/tx': + specifier: workspace:* + version: link:../../packages/tx + '@polkadot-apps/utils': + specifier: workspace:* + version: link:../../packages/utils + '@polkadot-labs/hdkd-helpers': + specifier: 'catalog:' + version: 0.0.27 + bulletin-deploy: + specifier: 'catalog:' + version: 0.6.4(@polkadot-api/ink-contracts@0.6.0)(@polkadot/util@13.5.9)(jiti@2.6.1)(postcss@8.5.9)(rxjs@7.8.2)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) + commander: + specifier: 'catalog:' + version: 12.1.0 + polkadot-api: + specifier: 'catalog:' + version: 1.23.3(jiti@2.6.1)(postcss@8.5.9)(rxjs@7.8.2)(tsx@4.21.0)(yaml@2.8.3) + ws: + specifier: 'catalog:' + version: 8.20.0 + devDependencies: + typescript: + specifier: 'catalog:' + version: 5.9.3 + examples/bulletin-demo: dependencies: '@novasamatech/host-api': @@ -735,10 +793,10 @@ importers: dependencies: '@novasamatech/host-papp': specifier: 0.6.17 - version: 0.6.17(esbuild@0.25.12)(jiti@2.6.1)(postcss@8.5.9)(rxjs@7.8.2)(tsx@4.21.0)(yaml@2.8.3) + version: 0.6.17(jiti@2.6.1)(postcss@8.5.9)(rxjs@7.8.2)(tsx@4.21.0)(yaml@2.8.3) '@novasamatech/statement-store': specifier: 0.6.17 - version: 0.6.17(esbuild@0.25.12)(jiti@2.6.1)(postcss@8.5.9)(rxjs@7.8.2)(tsx@4.21.0)(yaml@2.8.3) + version: 0.6.17(jiti@2.6.1)(postcss@8.5.9)(rxjs@7.8.2)(tsx@4.21.0)(yaml@2.8.3) '@novasamatech/storage-adapter': specifier: 0.6.17 version: 0.6.17 @@ -991,6 +1049,23 @@ packages: peerDependencies: commander: ~14.0.0 + '@dotdm/cdm@0.5.4': + resolution: {integrity: sha512-89tu3LgQrgnZkhFPr+aYNVEp6rNTOtuE1FwAlrt/Ji/tbNFzTPJ58yfA87V8TOXGEv1Yh5kPHNHeFuH8RiEbEA==} + + '@dotdm/contracts@0.4.0': + resolution: {integrity: sha512-1TfEmMQm5nmjMXBKSFqUKDnhaAC54D2xkvXk2lvv+r+Z7novFJSCylH7FxmiGJw7MI3V4MYl3TJB/Ka37oYgRg==} + + '@dotdm/descriptors@0.1.9': + resolution: {integrity: sha512-1bx4GD98+ASu64WhT0RHLjf9/Kihp0+YH12IOixi0ngF6G3bK2fXciZPrhklaw0oGeB2XOjH/N8sZ2oBA6lSCQ==} + peerDependencies: + polkadot-api: '>=1.21.0' + + '@dotdm/env@0.3.2': + resolution: {integrity: sha512-wdxuc+CiwiM3kH0WzwuNLCXLxCeKedYa5SnXhRH0J0kjWyw2BQwD05kQkbIkbPRnBAQ+h45uyzxmldR1Cebe8w==} + + '@dotdm/utils@0.3.1': + resolution: {integrity: sha512-pT7E+6opUlulMNJ2ZxdXg7EOB+DtIWgQc7BQ6wYrUUbKKsa1Oy1lrrdrHPmTethuJM2y96BJArmdW1Ma2nPy/Q==} + '@emnapi/runtime@1.9.2': resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==} @@ -1476,6 +1551,10 @@ packages: '@types/node': optional: true + '@ipld/dag-pb@4.1.5': + resolution: {integrity: sha512-w4PZ2yPqvNmlAir7/2hsCRMqny1EY5jj26iZcSgxREJexmbAc2FI21jp26MqiNdfgAxvkCnf2N/TJI18GaDNwA==} + engines: {node: '>=16.0.0', npm: '>=7.0.0'} + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -1634,10 +1713,194 @@ packages: '@novasamatech/storage-adapter@0.6.17': resolution: {integrity: sha512-J8xKl0dshS/r8zcrpdjQcqB74d7Y1nISVPTBPS5/Ael4zv/TOi2MKM7SE/wwRkJ6qJsvWJMNHodrm75f8YWHlQ==} + '@opentelemetry/api-logs@0.57.2': + resolution: {integrity: sha512-uIX52NnTM0iBh84MShlpouI7UKqkZ7MrUszTmaypHBu4r7NofznSnQRfJ+uUeDtQDj6w8eFGg5KBLDAwAPz1+A==} + engines: {node: '>=14'} + '@opentelemetry/api@1.9.1': resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==} engines: {node: '>=8.0.0'} + '@opentelemetry/context-async-hooks@1.30.1': + resolution: {integrity: sha512-s5vvxXPVdjqS3kTLKMeBMvop9hbWkwzBpu+mUO2M7sZtlkyDJGwFe33wRKnbaYDo8ExRVBIIdwIGrqpxHuKttA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/core@1.30.1': + resolution: {integrity: sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/instrumentation-amqplib@0.46.1': + resolution: {integrity: sha512-AyXVnlCf/xV3K/rNumzKxZqsULyITJH6OVLiW6730JPRqWA7Zc9bvYoVNpN6iOpTU8CasH34SU/ksVJmObFibQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-connect@0.43.1': + resolution: {integrity: sha512-ht7YGWQuV5BopMcw5Q2hXn3I8eG8TH0J/kc/GMcW4CuNTgiP6wCu44BOnucJWL3CmFWaRHI//vWyAhaC8BwePw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-dataloader@0.16.1': + resolution: {integrity: sha512-K/qU4CjnzOpNkkKO4DfCLSQshejRNAJtd4esgigo/50nxCB6XCyi1dhAblUHM9jG5dRm8eu0FB+t87nIo99LYQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-express@0.47.1': + resolution: {integrity: sha512-QNXPTWteDclR2B4pDFpz0TNghgB33UMjUt14B+BZPmtH1MwUFAfLHBaP5If0Z5NZC+jaH8oF2glgYjrmhZWmSw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-fs@0.19.1': + resolution: {integrity: sha512-6g0FhB3B9UobAR60BGTcXg4IHZ6aaYJzp0Ki5FhnxyAPt8Ns+9SSvgcrnsN2eGmk3RWG5vYycUGOEApycQL24A==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-generic-pool@0.43.1': + resolution: {integrity: sha512-M6qGYsp1cURtvVLGDrPPZemMFEbuMmCXgQYTReC/IbimV5sGrLBjB+/hANUpRZjX67nGLdKSVLZuQQAiNz+sww==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-graphql@0.47.1': + resolution: {integrity: sha512-EGQRWMGqwiuVma8ZLAZnExQ7sBvbOx0N/AE/nlafISPs8S+QtXX+Viy6dcQwVWwYHQPAcuY3bFt3xgoAwb4ZNQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-hapi@0.45.2': + resolution: {integrity: sha512-7Ehow/7Wp3aoyCrZwQpU7a2CnoMq0XhIcioFuKjBb0PLYfBfmTsFTUyatlHu0fRxhwcRsSQRTvEhmZu8CppBpQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-http@0.57.2': + resolution: {integrity: sha512-1Uz5iJ9ZAlFOiPuwYg29Bf7bJJc/GeoeJIFKJYQf67nTVKFe8RHbEtxgkOmK4UGZNHKXcpW4P8cWBYzBn1USpg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-ioredis@0.47.1': + resolution: {integrity: sha512-OtFGSN+kgk/aoKgdkKQnBsQFDiG8WdCxu+UrHr0bXScdAmtSzLSraLo7wFIb25RVHfRWvzI5kZomqJYEg/l1iA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-kafkajs@0.7.1': + resolution: {integrity: sha512-OtjaKs8H7oysfErajdYr1yuWSjMAectT7Dwr+axIoZqT9lmEOkD/H/3rgAs8h/NIuEi2imSXD+vL4MZtOuJfqQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-knex@0.44.1': + resolution: {integrity: sha512-U4dQxkNhvPexffjEmGwCq68FuftFK15JgUF05y/HlK3M6W/G2iEaACIfXdSnwVNe9Qh0sPfw8LbOPxrWzGWGMQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-koa@0.47.1': + resolution: {integrity: sha512-l/c+Z9F86cOiPJUllUCt09v+kICKvT+Vg1vOAJHtHPsJIzurGayucfCMq2acd/A/yxeNWunl9d9eqZ0G+XiI6A==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-lru-memoizer@0.44.1': + resolution: {integrity: sha512-5MPkYCvG2yw7WONEjYj5lr5JFehTobW7wX+ZUFy81oF2lr9IPfZk9qO+FTaM0bGEiymwfLwKe6jE15nHn1nmHg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-mongodb@0.52.0': + resolution: {integrity: sha512-1xmAqOtRUQGR7QfJFfGV/M2kC7wmI2WgZdpru8hJl3S0r4hW0n3OQpEHlSGXJAaNFyvT+ilnwkT+g5L4ljHR6g==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-mongoose@0.46.1': + resolution: {integrity: sha512-3kINtW1LUTPkiXFRSSBmva1SXzS/72we/jL22N+BnF3DFcoewkdkHPYOIdAAk9gSicJ4d5Ojtt1/HeibEc5OQg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-mysql2@0.45.2': + resolution: {integrity: sha512-h6Ad60FjCYdJZ5DTz1Lk2VmQsShiViKe0G7sYikb0GHI0NVvApp2XQNRHNjEMz87roFttGPLHOYVPlfy+yVIhQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-mysql@0.45.1': + resolution: {integrity: sha512-TKp4hQ8iKQsY7vnp/j0yJJ4ZsP109Ht6l4RHTj0lNEG1TfgTrIH5vJMbgmoYXWzNHAqBH2e7fncN12p3BP8LFg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-pg@0.51.1': + resolution: {integrity: sha512-QxgjSrxyWZc7Vk+qGSfsejPVFL1AgAJdSBMYZdDUbwg730D09ub3PXScB9d04vIqPriZ+0dqzjmQx0yWKiCi2Q==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-redis-4@0.46.1': + resolution: {integrity: sha512-UMqleEoabYMsWoTkqyt9WAzXwZ4BlFZHO40wr3d5ZvtjKCHlD4YXLm+6OLCeIi/HkX7EXvQaz8gtAwkwwSEvcQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-tedious@0.18.1': + resolution: {integrity: sha512-5Cuy/nj0HBaH+ZJ4leuD7RjgvA844aY2WW+B5uLcWtxGjRZl3MNLuxnNg5DYWZNPO+NafSSnra0q49KWAHsKBg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-undici@0.10.1': + resolution: {integrity: sha512-rkOGikPEyRpMCmNu9AQuV5dtRlDmJp2dK5sw8roVshAGoB6hH/3QjDtRhdwd75SsJwgynWUNRUYe0wAkTo16tQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.7.0 + + '@opentelemetry/instrumentation@0.57.2': + resolution: {integrity: sha512-BdBGhQBh8IjZ2oIIX6F2/Q3LKm/FDDKi6ccYKcBTeilh6SNdNKveDOLk73BkSJjQLJk6qe4Yh+hHw1UPhCDdrg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/redis-common@0.36.2': + resolution: {integrity: sha512-faYX1N0gpLhej/6nyp6bgRjzAKXn5GOEMYY7YhciSfCoITAktLUtQ36d24QEWNA1/WA1y6qQunCe0OhHRkVl9g==} + engines: {node: '>=14'} + + '@opentelemetry/resources@1.30.1': + resolution: {integrity: sha512-5UxZqiAgLYGFjS4s9qm5mBVo433u+dSPUFWVWXmLAD4wB65oMCoXaJP1KJa9DIYYMeHu3z4BZcStG3LC593cWA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/sdk-trace-base@1.30.1': + resolution: {integrity: sha512-jVPgBbH1gCy2Lb7X0AVQ8XAfgg0pJ4nvl8/IiQA6nxOsPvS+0zMJaFSs2ltXe0J6C8dqjcnpyqINDJmU30+uOg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/semantic-conventions@1.28.0': + resolution: {integrity: sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==} + engines: {node: '>=14'} + + '@opentelemetry/semantic-conventions@1.40.0': + resolution: {integrity: sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==} + engines: {node: '>=14'} + + '@opentelemetry/sql-common@0.40.1': + resolution: {integrity: sha512-nSDlnHSqzC3pXn/wZEZVLuAuJ1MYMXPBwtv2qAbCa3847SaHItdE7SzUq/Jtb0KZmh1zfAbNi3AAMjztTT4Ugg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@parity/host-api-test-sdk@0.6.0': resolution: {integrity: sha512-FNpnTiWaVwae6hrjKM6ytT2ISGUDhm232KuAwhm1ZOlWzfGWCKLEh5wtsd1xJbREEGH6Ixk8JdaTQElJnsrdVw==} peerDependencies: @@ -1861,12 +2124,18 @@ packages: peerDependencies: rxjs: '>=7.8.0' + '@polkadot-labs/hdkd-helpers@0.0.26': + resolution: {integrity: sha512-mp3GCSiOQeh4aPt+DYBQq6UnX/tKgYUH5F75knjW3ATSA90ifEEWWjRan0Bddt4QKYKamaDGadK9GbVREgzQFw==} + '@polkadot-labs/hdkd-helpers@0.0.27': resolution: {integrity: sha512-GTSj/Mw5kwtZbefvq2BhvBnHvs7AY4OnJgppO0kE2S/AuDbD6288C9rmO6qwMNmiNVX8OrYMWaJcs46Mt1UbBw==} '@polkadot-labs/hdkd-helpers@0.0.28': resolution: {integrity: sha512-kENij83Dr76RrcfsJmzcqNhThjWTPtzregb/i6o50kH5n1wkaE58/8PFahMnGVc9Trzk7jxfGSNQphQNwq2emg==} + '@polkadot-labs/hdkd@0.0.25': + resolution: {integrity: sha512-+yZJC1TE4ZKdfoILw8nGxu3H/klrYXm9GdVB0kcyQDecq320ThUmM1M4l8d1F/3QD0Nez9NwHi9t5B++OgJU5A==} + '@polkadot-labs/hdkd@0.0.26': resolution: {integrity: sha512-9B+egs7pIwmaxi3X7XBbruxS40aVbcdI1iIqSxfSOf4+RS52E+sgd3MW/LECWrHSof2h4ngcdsXRrOl95FVV8g==} @@ -1893,6 +2162,13 @@ packages: '@polkadot/api': '*' '@polkadot/util': '*' + '@polkadot/keyring@13.5.9': + resolution: {integrity: sha512-bMCpHDN7U8ytxawjBZ89/he5s3AmEZuOdkM/ABcorh/flXNPfyghjFK27Gy4OKoFxX52yJ2sTHR4NxM87GuFXQ==} + engines: {node: '>=18'} + peerDependencies: + '@polkadot/util': 13.5.9 + '@polkadot/util-crypto': 13.5.9 + '@polkadot/keyring@14.0.3': resolution: {integrity: sha512-ozp1dQwaHCjgX/fpTTORmHjxdUNQnyiTVJszpzUaUpvtH/IGZhSU/mSHXMqNETS/g57vQa7NatIDcWfyR9abyA==} engines: {node: '>=18'} @@ -2057,6 +2333,11 @@ packages: resolution: {integrity: sha512-tOPdkMye3iuXnuFtdNg5+iSu7Cz9LRL8z5psMuZpUpThMYChGsS2pDFtNvXOKU8ohhO+frY9VdJ9VBg1WL9Iug==} engines: {node: '>=18'} + '@prisma/instrumentation@6.11.1': + resolution: {integrity: sha512-mrZOev24EDhnefmnZX7WVVT7v+r9LttPRqf54ONvj6re4XMF7wFTpK2tLJi4XHB7fFp/6xhYbgRel8YV7gQiyA==} + peerDependencies: + '@opentelemetry/api': ^1.8 + '@rollup/rollup-android-arm-eabi@4.60.1': resolution: {integrity: sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==} cpu: [arm] @@ -2215,6 +2496,10 @@ packages: '@scure/sr25519@0.2.0': resolution: {integrity: sha512-uUuLP7Z126XdSizKtrCGqYyR3b3hYtJ6Fg/XFUXmc2//k2aXHDLqZwFeXxL97gg4XydPROPVnuaHGF2+xriSKg==} + '@scure/sr25519@0.3.0': + resolution: {integrity: sha512-SKsinX2sImunfcsH3seGrwH/OayBwwaJqVN8J1cJBNRCfbBq5q0jyTKGa9PcW1HWv9vXT6Yuq41JsxFLvF59ew==} + engines: {node: '>= 20.19.0'} + '@scure/sr25519@1.0.0': resolution: {integrity: sha512-b+uhK5akMINXZP95F3gJGcb5CMKYxf+q55fwMl0GoBwZDbWolmGNi1FrBSwuaZX5AhqS2byHiAueZgtDNpot2A==} engines: {node: '>= 20.19.0'} @@ -2222,6 +2507,36 @@ packages: '@sec-ant/readable-stream@0.4.1': resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + '@sentry/core@9.47.1': + resolution: {integrity: sha512-KX62+qIt4xgy8eHKHiikfhz2p5fOciXd0Cl+dNzhgPFq8klq4MGMNaf148GB3M/vBqP4nw/eFvRMAayFCgdRQw==} + engines: {node: '>=18'} + + '@sentry/node-core@9.47.1': + resolution: {integrity: sha512-7TEOiCGkyShJ8CKtsri9lbgMCbB+qNts2Xq37itiMPN2m+lIukK3OX//L8DC5nfKYZlgikrefS63/vJtm669hQ==} + engines: {node: '>=18'} + peerDependencies: + '@opentelemetry/api': ^1.9.0 + '@opentelemetry/context-async-hooks': ^1.30.1 || ^2.0.0 + '@opentelemetry/core': ^1.30.1 || ^2.0.0 + '@opentelemetry/instrumentation': '>=0.57.1 <1' + '@opentelemetry/resources': ^1.30.1 || ^2.0.0 + '@opentelemetry/sdk-trace-base': ^1.30.1 || ^2.0.0 + '@opentelemetry/semantic-conventions': ^1.34.0 + + '@sentry/node@9.47.1': + resolution: {integrity: sha512-CDbkasBz3fnWRKSFs6mmaRepM2pa+tbZkrqhPWifFfIkJDidtVW40p6OnquTvPXyPAszCnDZRnZT14xyvNmKPQ==} + engines: {node: '>=18'} + + '@sentry/opentelemetry@9.47.1': + resolution: {integrity: sha512-STtFpjF7lwzeoedDJV+5XA6P89BfmFwFftmHSGSe3UTI8z8IoiR5yB6X2vCjSPvXlfeOs13qCNNCEZyznxM8Xw==} + engines: {node: '>=18'} + peerDependencies: + '@opentelemetry/api': ^1.9.0 + '@opentelemetry/context-async-hooks': ^1.30.1 || ^2.0.0 + '@opentelemetry/core': ^1.30.1 || ^2.0.0 + '@opentelemetry/sdk-trace-base': ^1.30.1 || ^2.0.0 + '@opentelemetry/semantic-conventions': ^1.34.0 + '@shikijs/engine-oniguruma@3.23.0': resolution: {integrity: sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g==} @@ -2390,6 +2705,9 @@ packages: '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} @@ -2399,6 +2717,9 @@ packages: '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + '@types/mysql@2.15.26': + resolution: {integrity: sha512-DSLCOXhkvfS5WNNPbfn2KdICAmk8lLc+/PNvnPnF7gOdMZCxopXduqv0OQ13y/yA/zXTSikZZqVgybUxOEg6YQ==} + '@types/node@12.20.55': resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} @@ -2411,6 +2732,12 @@ packages: '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} + '@types/pg-pool@2.0.6': + resolution: {integrity: sha512-TaAUE5rq2VQYxab5Ts7WZhKNmuN78Q6PiFonTDdpbx8a1H0M1vhy3rhiMjl+e2iHmogyMw7jZF4FrE6eJUy5HQ==} + + '@types/pg@8.6.1': + resolution: {integrity: sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w==} + '@types/qrcode@1.5.6': resolution: {integrity: sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==} @@ -2422,6 +2749,12 @@ packages: '@types/react@19.2.14': resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} + '@types/shimmer@1.2.0': + resolution: {integrity: sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==} + + '@types/tedious@4.0.14': + resolution: {integrity: sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==} + '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} @@ -2473,6 +2806,11 @@ packages: zod: optional: true + acorn-import-attributes@1.9.5: + resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} + peerDependencies: + acorn: ^8 + acorn@8.16.0: resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} engines: {node: '>=0.4.0'} @@ -2550,6 +2888,11 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + bulletin-deploy@0.6.4: + resolution: {integrity: sha512-HunhqWHZ4tSuMZgtRLkcsMZhjnztqzI+fkg0R6QLsMYHKekLpyjlHiYFDD1irUTzH/mraSQ6o//PJ2i2CMKd1Q==} + engines: {node: '>=22'} + hasBin: true + bundle-require@5.1.0: resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -2586,6 +2929,9 @@ packages: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} + cjs-module-lexer@1.4.3: + resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} + cli-cursor@5.0.0: resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} engines: {node: '>=18'} @@ -2607,6 +2953,10 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + commander@12.1.0: + resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} + engines: {node: '>=18'} + commander@14.0.3: resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} engines: {node: '>=20'} @@ -2703,6 +3053,10 @@ packages: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} @@ -2788,6 +3142,9 @@ packages: resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} engines: {node: '>=12.20.0'} + forwarded-parse@2.1.2: + resolution: {integrity: sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==} + fs-extra@7.0.1: resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} engines: {node: '>=6 <7 || >=8'} @@ -2809,6 +3166,9 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -2848,6 +3208,10 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + hosted-git-info@7.0.2: resolution: {integrity: sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==} engines: {node: ^16.14.0 || >=18.0.0} @@ -2875,6 +3239,9 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} + import-in-the-middle@1.15.0: + resolution: {integrity: sha512-bpQy+CrsRmYmoPMAE/0G33iwRqwW4ouqdRg8jgbH3aKuCtOc8lxgmYXg2dMM92CRiGP660EtBcymH/eVUpCSaA==} + imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} @@ -2883,6 +3250,13 @@ packages: resolution: {integrity: sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==} engines: {node: '>=18'} + ipfs-unixfs@11.2.5: + resolution: {integrity: sha512-uasYJ0GLPbViaTFsOLnL9YPjX5VmhnqtWRriogAHOe4ApmIi9VAOFBzgDHsUW2ub4pEa/EysbtWk126g2vkU/g==} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -3158,6 +3532,9 @@ packages: resolution: {integrity: sha512-qxBgB7Qa2sEQgHFjj0dSigq7fX4k6Saisd5Nelwp2q8mlbAFh5dHV9JTTlF8viYJLSSWgMCZFUom8PJcMNBoJw==} engines: {node: '>= 8'} + module-details-from-path@1.0.4: + resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==} + mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} @@ -3307,6 +3684,9 @@ packages: resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} engines: {node: '>=12'} + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + path-scurry@1.11.1: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} @@ -3322,6 +3702,17 @@ packages: resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} engines: {node: '>= 14.16'} + pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + pg-protocol@1.13.0: + resolution: {integrity: sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==} + + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -3396,6 +3787,22 @@ packages: resolution: {integrity: sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==} engines: {node: ^10 || ^12 || >=14} + postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + postgres-bytea@1.0.1: + resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==} + engines: {node: '>=0.10.0'} + + postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + prettier@2.8.8: resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} engines: {node: '>=10.13.0'} @@ -3409,6 +3816,9 @@ packages: resolution: {integrity: sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==} engines: {node: '>= 8'} + protons-runtime@5.6.0: + resolution: {integrity: sha512-/Kde+sB9DsMFrddJT/UZWe6XqvL7SL5dbag/DBCElFKhkwDj7XKt53S+mzLyaDP5OqS0wXjV5SA572uWDaT0Hg==} + punycode.js@2.3.1: resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} engines: {node: '>=6'} @@ -3457,6 +3867,10 @@ packages: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} + require-in-the-middle@7.5.2: + resolution: {integrity: sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==} + engines: {node: '>=8.6.0'} + require-main-filename@2.0.0: resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} @@ -3467,6 +3881,11 @@ packages: resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + resolve@1.22.12: + resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} + engines: {node: '>= 0.4'} + hasBin: true + restore-cursor@5.1.0: resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} engines: {node: '>=18'} @@ -3526,6 +3945,9 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + shimmer@1.2.1: + resolution: {integrity: sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -3637,6 +4059,10 @@ packages: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + tagged-tag@1.0.0: resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} engines: {node: '>=20'} @@ -3770,6 +4196,15 @@ packages: ufo@1.6.3: resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} + uint8-varint@2.0.4: + resolution: {integrity: sha512-FwpTa7ZGA/f/EssWAb5/YV6pHgVF1fViKdW8cWaEarjB8t7NyofSWBdOTyFPaGuUG4gx3v1O3PQ8etsiOs3lcw==} + + uint8arraylist@2.4.9: + resolution: {integrity: sha512-KxWjyEFzchzik3aoQlK66oaoxIReoMo5bQRm1fcjBUZvE8xv/tyR3CTKhjh6K/faV8VaF6hd5pjr45CzbwuwkA==} + + uint8arrays@5.1.1: + resolution: {integrity: sha512-9muQwa4wZG4dKi9gMAIBtnk2Pw87SRpvWTH6lOGm19V2Uqxr4uomUf2PGqPnWc+qs06sN8owUU4jfcoWOcfwVQ==} + undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} @@ -3960,6 +4395,10 @@ packages: utf-8-validate: optional: true + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + y18n@4.0.3: resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} @@ -4273,28 +4712,103 @@ snapshots: dependencies: commander: 14.0.3 - '@emnapi/runtime@1.9.2': + '@dotdm/cdm@0.5.4(@polkadot-api/ink-contracts@0.6.0)(jiti@2.6.1)(postcss@8.5.9)(rxjs@7.8.2)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)': dependencies: - tslib: 2.8.1 - optional: true - - '@esbuild/aix-ppc64@0.25.12': - optional: true - - '@esbuild/aix-ppc64@0.27.7': - optional: true - - '@esbuild/android-arm64@0.25.12': - optional: true + '@dotdm/contracts': 0.4.0(@polkadot-api/ink-contracts@0.6.0)(jiti@2.6.1)(postcss@8.5.9)(rxjs@7.8.2)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) + '@dotdm/env': 0.3.2(jiti@2.6.1)(postcss@8.5.9)(rxjs@7.8.2)(tsx@4.21.0)(yaml@2.8.3) + '@dotdm/utils': 0.3.1 + '@polkadot-api/sdk-ink': 0.6.2(@polkadot-api/ink-contracts@0.6.0)(polkadot-api@1.23.3(jiti@2.6.1)(postcss@8.5.9)(rxjs@7.8.2)(tsx@4.21.0)(yaml@2.8.3))(rxjs@7.8.2)(typescript@5.9.3) + polkadot-api: 1.23.3(jiti@2.6.1)(postcss@8.5.9)(rxjs@7.8.2)(tsx@4.21.0)(yaml@2.8.3) + transitivePeerDependencies: + - '@microsoft/api-extractor' + - '@polkadot-api/ink-contracts' + - '@swc/core' + - bufferutil + - jiti + - postcss + - rxjs + - supports-color + - tsx + - typescript + - utf-8-validate + - yaml + - zod - '@esbuild/android-arm64@0.27.7': - optional: true + '@dotdm/contracts@0.4.0(@polkadot-api/ink-contracts@0.6.0)(jiti@2.6.1)(postcss@8.5.9)(rxjs@7.8.2)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)': + dependencies: + '@dotdm/descriptors': 0.1.9(polkadot-api@1.23.3(jiti@2.6.1)(postcss@8.5.9)(rxjs@7.8.2)(tsx@4.21.0)(yaml@2.8.3)) + '@dotdm/env': 0.3.2(jiti@2.6.1)(postcss@8.5.9)(rxjs@7.8.2)(tsx@4.21.0)(yaml@2.8.3) + '@dotdm/utils': 0.3.1 + '@noble/hashes': 2.2.0 + '@polkadot-api/sdk-ink': 0.6.2(@polkadot-api/ink-contracts@0.6.0)(polkadot-api@1.23.3(jiti@2.6.1)(postcss@8.5.9)(rxjs@7.8.2)(tsx@4.21.0)(yaml@2.8.3))(rxjs@7.8.2)(typescript@5.9.3) + multiformats: 13.4.2 + polkadot-api: 1.23.3(jiti@2.6.1)(postcss@8.5.9)(rxjs@7.8.2)(tsx@4.21.0)(yaml@2.8.3) + transitivePeerDependencies: + - '@microsoft/api-extractor' + - '@polkadot-api/ink-contracts' + - '@swc/core' + - bufferutil + - jiti + - postcss + - rxjs + - supports-color + - tsx + - typescript + - utf-8-validate + - yaml + - zod - '@esbuild/android-arm@0.25.12': - optional: true + '@dotdm/descriptors@0.1.9(polkadot-api@1.23.3(jiti@2.6.1)(postcss@8.5.9)(rxjs@7.8.2)(tsx@4.21.0)(yaml@2.8.3))': + dependencies: + polkadot-api: 1.23.3(jiti@2.6.1)(postcss@8.5.9)(rxjs@7.8.2)(tsx@4.21.0)(yaml@2.8.3) - '@esbuild/android-arm@0.27.7': - optional: true + '@dotdm/env@0.3.2(jiti@2.6.1)(postcss@8.5.9)(rxjs@7.8.2)(tsx@4.21.0)(yaml@2.8.3)': + dependencies: + '@dotdm/descriptors': 0.1.9(polkadot-api@1.23.3(jiti@2.6.1)(postcss@8.5.9)(rxjs@7.8.2)(tsx@4.21.0)(yaml@2.8.3)) + '@dotdm/utils': 0.3.1 + '@polkadot-labs/hdkd': 0.0.26 + '@polkadot-labs/hdkd-helpers': 0.0.27 + polkadot-api: 1.23.3(jiti@2.6.1)(postcss@8.5.9)(rxjs@7.8.2)(tsx@4.21.0)(yaml@2.8.3) + smoldot: 2.0.40 + transitivePeerDependencies: + - '@microsoft/api-extractor' + - '@swc/core' + - bufferutil + - jiti + - postcss + - rxjs + - supports-color + - tsx + - utf-8-validate + - yaml + + '@dotdm/utils@0.3.1': + dependencies: + '@polkadot-labs/hdkd': 0.0.26 + '@polkadot-labs/hdkd-helpers': 0.0.27 + + '@emnapi/runtime@1.9.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.25.12': + optional: true + + '@esbuild/aix-ppc64@0.27.7': + optional: true + + '@esbuild/android-arm64@0.25.12': + optional: true + + '@esbuild/android-arm64@0.27.7': + optional: true + + '@esbuild/android-arm@0.25.12': + optional: true + + '@esbuild/android-arm@0.27.7': + optional: true '@esbuild/android-x64@0.25.12': optional: true @@ -4548,6 +5062,10 @@ snapshots: optionalDependencies: '@types/node': 25.6.0 + '@ipld/dag-pb@4.1.5': + dependencies: + multiformats: 13.4.2 + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -4669,14 +5187,14 @@ snapshots: neverthrow: 8.2.0 scale-ts: 1.6.1 - '@novasamatech/host-papp@0.6.17(esbuild@0.25.12)(jiti@2.6.1)(postcss@8.5.9)(rxjs@7.8.2)(tsx@4.21.0)(yaml@2.8.3)': + '@novasamatech/host-papp@0.6.17(jiti@2.6.1)(postcss@8.5.9)(rxjs@7.8.2)(tsx@4.21.0)(yaml@2.8.3)': dependencies: '@noble/ciphers': 2.1.1 '@noble/curves': 2.0.1 '@noble/hashes': 2.0.1 '@novasamatech/host-api': 0.6.17 '@novasamatech/scale': 0.6.17 - '@novasamatech/statement-store': 0.6.17(esbuild@0.25.12)(jiti@2.6.1)(postcss@8.5.9)(rxjs@7.8.2)(tsx@4.21.0)(yaml@2.8.3) + '@novasamatech/statement-store': 0.6.17(jiti@2.6.1)(postcss@8.5.9)(rxjs@7.8.2)(tsx@4.21.0)(yaml@2.8.3) '@novasamatech/storage-adapter': 0.6.17 '@polkadot-api/substrate-bindings': 0.17.0 '@polkadot-api/utils': 0.2.0 @@ -4720,18 +5238,6 @@ snapshots: '@polkadot-api/utils': 0.2.0 scale-ts: 1.6.1 - '@novasamatech/sdk-statement@0.5.0(esbuild@0.25.12)(rxjs@7.8.2)': - dependencies: - '@polkadot-api/substrate-bindings': 0.19.0 - '@polkadot-api/utils': 0.3.0 - polkadot-api: 2.0.0(esbuild@0.25.12)(rxjs@7.8.2) - transitivePeerDependencies: - - bufferutil - - esbuild - - rxjs - - supports-color - - utf-8-validate - '@novasamatech/sdk-statement@0.5.0(esbuild@0.27.7)(rxjs@7.8.2)': dependencies: '@polkadot-api/substrate-bindings': 0.19.0 @@ -4744,11 +5250,11 @@ snapshots: - supports-color - utf-8-validate - '@novasamatech/statement-store@0.6.17(esbuild@0.25.12)(jiti@2.6.1)(postcss@8.5.9)(rxjs@7.8.2)(tsx@4.21.0)(yaml@2.8.3)': + '@novasamatech/statement-store@0.6.17(jiti@2.6.1)(postcss@8.5.9)(rxjs@7.8.2)(tsx@4.21.0)(yaml@2.8.3)': dependencies: '@noble/ciphers': 2.1.1 '@noble/hashes': 2.0.1 - '@novasamatech/sdk-statement': 0.5.0(esbuild@0.25.12)(rxjs@7.8.2) + '@novasamatech/sdk-statement': 0.5.0(esbuild@0.27.7)(rxjs@7.8.2) '@polkadot-api/substrate-bindings': 0.17.0 '@polkadot-api/substrate-client': 0.5.0 '@polkadot-api/utils': 0.2.0 @@ -4775,8 +5281,247 @@ snapshots: nanoevents: 9.1.0 neverthrow: 8.2.0 - '@opentelemetry/api@1.9.1': - optional: true + '@opentelemetry/api-logs@0.57.2': + dependencies: + '@opentelemetry/api': 1.9.1 + + '@opentelemetry/api@1.9.1': {} + + '@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + + '@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/semantic-conventions': 1.28.0 + + '@opentelemetry/instrumentation-amqplib@0.46.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-connect@0.43.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + '@types/connect': 3.4.38 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-dataloader@0.16.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.1) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-express@0.47.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-fs@0.19.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.1) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-generic-pool@0.43.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.1) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-graphql@0.47.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.1) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-hapi@0.45.2(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-http@0.57.2(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.28.0 + forwarded-parse: 2.1.2 + semver: 7.7.4 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-ioredis@0.47.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.1) + '@opentelemetry/redis-common': 0.36.2 + '@opentelemetry/semantic-conventions': 1.40.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-kafkajs@0.7.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-knex@0.44.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-koa@0.47.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-lru-memoizer@0.44.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.1) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-mongodb@0.52.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-mongoose@0.46.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-mysql2@0.45.2(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + '@opentelemetry/sql-common': 0.40.1(@opentelemetry/api@1.9.1) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-mysql@0.45.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + '@types/mysql': 2.15.26 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-pg@0.51.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + '@opentelemetry/sql-common': 0.40.1(@opentelemetry/api@1.9.1) + '@types/pg': 8.6.1 + '@types/pg-pool': 2.0.6 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-redis-4@0.46.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.1) + '@opentelemetry/redis-common': 0.36.2 + '@opentelemetry/semantic-conventions': 1.40.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-tedious@0.18.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + '@types/tedious': 4.0.14 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-undici@0.10.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.1) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.57.2 + '@types/shimmer': 1.2.0 + import-in-the-middle: 1.15.0 + require-in-the-middle: 7.5.2 + semver: 7.7.4 + shimmer: 1.2.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/redis-common@0.36.2': {} + + '@opentelemetry/resources@1.30.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.28.0 + + '@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.28.0 + + '@opentelemetry/semantic-conventions@1.28.0': {} + + '@opentelemetry/semantic-conventions@1.40.0': {} + + '@opentelemetry/sql-common@0.40.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) '@parity/host-api-test-sdk@0.6.0(@playwright/test@1.59.1)': dependencies: @@ -4831,41 +5576,6 @@ snapshots: - utf-8-validate - yaml - '@polkadot-api/cli@0.20.1(esbuild@0.25.12)': - dependencies: - '@commander-js/extra-typings': 14.0.0(commander@14.0.3) - '@polkadot-api/codegen': 0.22.2 - '@polkadot-api/ink-contracts': 0.6.0 - '@polkadot-api/json-rpc-provider': 0.2.0 - '@polkadot-api/known-chains': 0.11.1 - '@polkadot-api/metadata-compatibility': 0.6.0 - '@polkadot-api/observable-client': 0.18.3(rxjs@7.8.2) - '@polkadot-api/sm-provider': 0.3.0(@polkadot-api/smoldot@0.4.0) - '@polkadot-api/smoldot': 0.4.0 - '@polkadot-api/substrate-bindings': 0.20.0 - '@polkadot-api/substrate-client': 0.7.0 - '@polkadot-api/utils': 0.4.0 - '@polkadot-api/wasm-executor': 0.2.3 - '@polkadot-api/ws-middleware': 0.3.0(rxjs@7.8.2) - '@polkadot-api/ws-provider': 0.9.0(rxjs@7.8.2) - '@types/node': 25.6.0 - commander: 14.0.3 - execa: 9.6.1 - fs.promises.exists: 1.1.4 - ora: 9.3.0 - read-pkg: 10.1.0 - rollup: 4.60.1 - rollup-plugin-esbuild: 6.2.1(esbuild@0.25.12)(rollup@4.60.1) - rxjs: 7.8.2 - tsc-prog: 2.3.0(typescript@6.0.2) - typescript: 6.0.2 - write-package: 7.2.0 - transitivePeerDependencies: - - bufferutil - - esbuild - - supports-color - - utf-8-validate - '@polkadot-api/cli@0.20.1(esbuild@0.27.7)': dependencies: '@commander-js/extra-typings': 14.0.0(commander@14.0.3) @@ -5236,6 +5946,14 @@ snapshots: '@polkadot-api/utils': 0.4.0 rxjs: 7.8.2 + '@polkadot-labs/hdkd-helpers@0.0.26': + dependencies: + '@noble/curves': 2.2.0 + '@noble/hashes': 2.2.0 + '@scure/base': 2.0.0 + '@scure/sr25519': 0.3.0 + scale-ts: 1.6.1 + '@polkadot-labs/hdkd-helpers@0.0.27': dependencies: '@noble/curves': 2.2.0 @@ -5252,6 +5970,10 @@ snapshots: '@scure/sr25519': 1.0.0 scale-ts: 1.6.1 + '@polkadot-labs/hdkd@0.0.25': + dependencies: + '@polkadot-labs/hdkd-helpers': 0.0.28 + '@polkadot-labs/hdkd@0.0.26': dependencies: '@polkadot-labs/hdkd-helpers': 0.0.28 @@ -5337,6 +6059,12 @@ snapshots: - supports-color - utf-8-validate + '@polkadot/keyring@13.5.9(@polkadot/util-crypto@13.5.9(@polkadot/util@13.5.9))(@polkadot/util@13.5.9)': + dependencies: + '@polkadot/util': 13.5.9 + '@polkadot/util-crypto': 13.5.9(@polkadot/util@13.5.9) + tslib: 2.8.1 + '@polkadot/keyring@14.0.3(@polkadot/util-crypto@14.0.3(@polkadot/util@14.0.3))(@polkadot/util@14.0.3)': dependencies: '@polkadot/util': 14.0.3 @@ -5445,6 +6173,19 @@ snapshots: rxjs: 7.8.2 tslib: 2.8.1 + '@polkadot/util-crypto@13.5.9(@polkadot/util@13.5.9)': + dependencies: + '@noble/curves': 1.9.7 + '@noble/hashes': 1.8.0 + '@polkadot/networks': 13.5.9 + '@polkadot/util': 13.5.9 + '@polkadot/wasm-crypto': 7.5.4(@polkadot/util@13.5.9)(@polkadot/x-randomvalues@13.5.9(@polkadot/util@13.5.9)(@polkadot/wasm-util@7.5.4(@polkadot/util@13.5.9))) + '@polkadot/wasm-util': 7.5.4(@polkadot/util@13.5.9) + '@polkadot/x-bigint': 13.5.9 + '@polkadot/x-randomvalues': 13.5.9(@polkadot/util@13.5.9)(@polkadot/wasm-util@7.5.4(@polkadot/util@13.5.9)) + '@scure/base': 1.2.6 + tslib: 2.8.1 + '@polkadot/util-crypto@13.5.9(@polkadot/util@14.0.3)': dependencies: '@noble/curves': 1.9.7 @@ -5492,6 +6233,13 @@ snapshots: bn.js: 5.2.3 tslib: 2.8.1 + '@polkadot/wasm-bridge@7.5.4(@polkadot/util@13.5.9)(@polkadot/x-randomvalues@13.5.9(@polkadot/util@13.5.9)(@polkadot/wasm-util@7.5.4(@polkadot/util@13.5.9)))': + dependencies: + '@polkadot/util': 13.5.9 + '@polkadot/wasm-util': 7.5.4(@polkadot/util@13.5.9) + '@polkadot/x-randomvalues': 13.5.9(@polkadot/util@13.5.9)(@polkadot/wasm-util@7.5.4(@polkadot/util@13.5.9)) + tslib: 2.8.1 + '@polkadot/wasm-bridge@7.5.4(@polkadot/util@14.0.3)(@polkadot/x-randomvalues@13.5.9(@polkadot/util@14.0.3)(@polkadot/wasm-util@7.5.4(@polkadot/util@14.0.3)))': dependencies: '@polkadot/util': 14.0.3 @@ -5506,11 +6254,26 @@ snapshots: '@polkadot/x-randomvalues': 14.0.3(@polkadot/util@14.0.3)(@polkadot/wasm-util@7.5.4(@polkadot/util@14.0.3)) tslib: 2.8.1 + '@polkadot/wasm-crypto-asmjs@7.5.4(@polkadot/util@13.5.9)': + dependencies: + '@polkadot/util': 13.5.9 + tslib: 2.8.1 + '@polkadot/wasm-crypto-asmjs@7.5.4(@polkadot/util@14.0.3)': dependencies: '@polkadot/util': 14.0.3 tslib: 2.8.1 + '@polkadot/wasm-crypto-init@7.5.4(@polkadot/util@13.5.9)(@polkadot/x-randomvalues@13.5.9(@polkadot/util@13.5.9)(@polkadot/wasm-util@7.5.4(@polkadot/util@13.5.9)))': + dependencies: + '@polkadot/util': 13.5.9 + '@polkadot/wasm-bridge': 7.5.4(@polkadot/util@13.5.9)(@polkadot/x-randomvalues@13.5.9(@polkadot/util@13.5.9)(@polkadot/wasm-util@7.5.4(@polkadot/util@13.5.9))) + '@polkadot/wasm-crypto-asmjs': 7.5.4(@polkadot/util@13.5.9) + '@polkadot/wasm-crypto-wasm': 7.5.4(@polkadot/util@13.5.9) + '@polkadot/wasm-util': 7.5.4(@polkadot/util@13.5.9) + '@polkadot/x-randomvalues': 13.5.9(@polkadot/util@13.5.9)(@polkadot/wasm-util@7.5.4(@polkadot/util@13.5.9)) + tslib: 2.8.1 + '@polkadot/wasm-crypto-init@7.5.4(@polkadot/util@14.0.3)(@polkadot/x-randomvalues@13.5.9(@polkadot/util@14.0.3)(@polkadot/wasm-util@7.5.4(@polkadot/util@14.0.3)))': dependencies: '@polkadot/util': 14.0.3 @@ -5531,12 +6294,29 @@ snapshots: '@polkadot/x-randomvalues': 14.0.3(@polkadot/util@14.0.3)(@polkadot/wasm-util@7.5.4(@polkadot/util@14.0.3)) tslib: 2.8.1 + '@polkadot/wasm-crypto-wasm@7.5.4(@polkadot/util@13.5.9)': + dependencies: + '@polkadot/util': 13.5.9 + '@polkadot/wasm-util': 7.5.4(@polkadot/util@13.5.9) + tslib: 2.8.1 + '@polkadot/wasm-crypto-wasm@7.5.4(@polkadot/util@14.0.3)': dependencies: '@polkadot/util': 14.0.3 '@polkadot/wasm-util': 7.5.4(@polkadot/util@14.0.3) tslib: 2.8.1 + '@polkadot/wasm-crypto@7.5.4(@polkadot/util@13.5.9)(@polkadot/x-randomvalues@13.5.9(@polkadot/util@13.5.9)(@polkadot/wasm-util@7.5.4(@polkadot/util@13.5.9)))': + dependencies: + '@polkadot/util': 13.5.9 + '@polkadot/wasm-bridge': 7.5.4(@polkadot/util@13.5.9)(@polkadot/x-randomvalues@13.5.9(@polkadot/util@13.5.9)(@polkadot/wasm-util@7.5.4(@polkadot/util@13.5.9))) + '@polkadot/wasm-crypto-asmjs': 7.5.4(@polkadot/util@13.5.9) + '@polkadot/wasm-crypto-init': 7.5.4(@polkadot/util@13.5.9)(@polkadot/x-randomvalues@13.5.9(@polkadot/util@13.5.9)(@polkadot/wasm-util@7.5.4(@polkadot/util@13.5.9))) + '@polkadot/wasm-crypto-wasm': 7.5.4(@polkadot/util@13.5.9) + '@polkadot/wasm-util': 7.5.4(@polkadot/util@13.5.9) + '@polkadot/x-randomvalues': 13.5.9(@polkadot/util@13.5.9)(@polkadot/wasm-util@7.5.4(@polkadot/util@13.5.9)) + tslib: 2.8.1 + '@polkadot/wasm-crypto@7.5.4(@polkadot/util@14.0.3)(@polkadot/x-randomvalues@13.5.9(@polkadot/util@14.0.3)(@polkadot/wasm-util@7.5.4(@polkadot/util@14.0.3)))': dependencies: '@polkadot/util': 14.0.3 @@ -5559,6 +6339,11 @@ snapshots: '@polkadot/x-randomvalues': 14.0.3(@polkadot/util@14.0.3)(@polkadot/wasm-util@7.5.4(@polkadot/util@14.0.3)) tslib: 2.8.1 + '@polkadot/wasm-util@7.5.4(@polkadot/util@13.5.9)': + dependencies: + '@polkadot/util': 13.5.9 + tslib: 2.8.1 + '@polkadot/wasm-util@7.5.4(@polkadot/util@14.0.3)': dependencies: '@polkadot/util': 14.0.3 @@ -5588,6 +6373,13 @@ snapshots: dependencies: tslib: 2.8.1 + '@polkadot/x-randomvalues@13.5.9(@polkadot/util@13.5.9)(@polkadot/wasm-util@7.5.4(@polkadot/util@13.5.9))': + dependencies: + '@polkadot/util': 13.5.9 + '@polkadot/wasm-util': 7.5.4(@polkadot/util@13.5.9) + '@polkadot/x-global': 13.5.9 + tslib: 2.8.1 + '@polkadot/x-randomvalues@13.5.9(@polkadot/util@14.0.3)(@polkadot/wasm-util@7.5.4(@polkadot/util@14.0.3))': dependencies: '@polkadot/util': 14.0.3 @@ -5631,6 +6423,13 @@ snapshots: - bufferutil - utf-8-validate + '@prisma/instrumentation@6.11.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.1) + transitivePeerDependencies: + - supports-color + '@rollup/rollup-android-arm-eabi@4.60.1': optional: true @@ -5730,6 +6529,11 @@ snapshots: '@noble/curves': 1.9.7 '@noble/hashes': 1.8.0 + '@scure/sr25519@0.3.0': + dependencies: + '@noble/curves': 2.0.1 + '@noble/hashes': 2.0.1 + '@scure/sr25519@1.0.0': dependencies: '@noble/curves': 2.0.1 @@ -5737,6 +6541,70 @@ snapshots: '@sec-ant/readable-stream@0.4.1': {} + '@sentry/core@9.47.1': {} + + '@sentry/node-core@9.47.1(@opentelemetry/api@1.9.1)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.1))(@opentelemetry/resources@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.40.0)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/context-async-hooks': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + '@sentry/core': 9.47.1 + '@sentry/opentelemetry': 9.47.1(@opentelemetry/api@1.9.1)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.40.0) + import-in-the-middle: 1.15.0 + + '@sentry/node@9.47.1': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/context-async-hooks': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-amqplib': 0.46.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-connect': 0.43.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-dataloader': 0.16.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-express': 0.47.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-fs': 0.19.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-generic-pool': 0.43.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-graphql': 0.47.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-hapi': 0.45.2(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-http': 0.57.2(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-ioredis': 0.47.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-kafkajs': 0.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-knex': 0.44.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-koa': 0.47.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-lru-memoizer': 0.44.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-mongodb': 0.52.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-mongoose': 0.46.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-mysql': 0.45.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-mysql2': 0.45.2(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-pg': 0.51.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-redis-4': 0.46.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-tedious': 0.18.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-undici': 0.10.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + '@prisma/instrumentation': 6.11.1(@opentelemetry/api@1.9.1) + '@sentry/core': 9.47.1 + '@sentry/node-core': 9.47.1(@opentelemetry/api@1.9.1)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.1))(@opentelemetry/resources@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.40.0) + '@sentry/opentelemetry': 9.47.1(@opentelemetry/api@1.9.1)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.40.0) + import-in-the-middle: 1.15.0 + minimatch: 9.0.9 + transitivePeerDependencies: + - supports-color + + '@sentry/opentelemetry@9.47.1(@opentelemetry/api@1.9.1)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.40.0)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/context-async-hooks': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + '@sentry/core': 9.47.1 + '@shikijs/engine-oniguruma@3.23.0': dependencies: '@shikijs/types': 3.23.0 @@ -5890,6 +6758,10 @@ snapshots: '@types/deep-eql': 4.0.2 assertion-error: 2.0.1 + '@types/connect@3.4.38': + dependencies: + '@types/node': 25.6.0 + '@types/deep-eql@4.0.2': {} '@types/estree@1.0.8': {} @@ -5898,6 +6770,10 @@ snapshots: dependencies: '@types/unist': 3.0.3 + '@types/mysql@2.15.26': + dependencies: + '@types/node': 25.6.0 + '@types/node@12.20.55': {} '@types/node@24.12.2': @@ -5910,6 +6786,16 @@ snapshots: '@types/normalize-package-data@2.4.4': {} + '@types/pg-pool@2.0.6': + dependencies: + '@types/pg': 8.6.1 + + '@types/pg@8.6.1': + dependencies: + '@types/node': 25.6.0 + pg-protocol: 1.13.0 + pg-types: 2.2.0 + '@types/qrcode@1.5.6': dependencies: '@types/node': 25.6.0 @@ -5922,6 +6808,12 @@ snapshots: dependencies: csstype: 3.2.3 + '@types/shimmer@1.2.0': {} + + '@types/tedious@4.0.14': + dependencies: + '@types/node': 25.6.0 + '@types/unist@3.0.3': {} '@types/ws@8.18.1': @@ -5990,6 +6882,10 @@ snapshots: optionalDependencies: typescript: 5.9.3 + acorn-import-attributes@1.9.5(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + acorn@8.16.0: {} ansi-colors@4.1.3: {} @@ -6048,6 +6944,37 @@ snapshots: node-releases: 2.0.37 update-browserslist-db: 1.2.3(browserslist@4.28.2) + bulletin-deploy@0.6.4(@polkadot-api/ink-contracts@0.6.0)(@polkadot/util@13.5.9)(jiti@2.6.1)(postcss@8.5.9)(rxjs@7.8.2)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3): + dependencies: + '@dotdm/cdm': 0.5.4(@polkadot-api/ink-contracts@0.6.0)(jiti@2.6.1)(postcss@8.5.9)(rxjs@7.8.2)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) + '@ipld/dag-pb': 4.1.5 + '@noble/hashes': 1.8.0 + '@polkadot-api/substrate-bindings': 0.16.6 + '@polkadot-labs/hdkd': 0.0.25 + '@polkadot-labs/hdkd-helpers': 0.0.26 + '@polkadot/keyring': 13.5.9(@polkadot/util-crypto@13.5.9(@polkadot/util@13.5.9))(@polkadot/util@13.5.9) + '@polkadot/util-crypto': 13.5.9(@polkadot/util@13.5.9) + '@sentry/node': 9.47.1 + ipfs-unixfs: 11.2.5 + multiformats: 13.4.2 + polkadot-api: 1.23.3(jiti@2.6.1)(postcss@8.5.9)(rxjs@7.8.2)(tsx@4.21.0)(yaml@2.8.3) + viem: 2.47.16(typescript@5.9.3) + transitivePeerDependencies: + - '@microsoft/api-extractor' + - '@polkadot-api/ink-contracts' + - '@polkadot/util' + - '@swc/core' + - bufferutil + - jiti + - postcss + - rxjs + - supports-color + - tsx + - typescript + - utf-8-validate + - yaml + - zod + bundle-require@5.1.0(esbuild@0.25.12): dependencies: esbuild: 0.25.12 @@ -6077,6 +7004,8 @@ snapshots: dependencies: readdirp: 4.1.2 + cjs-module-lexer@1.4.3: {} + cli-cursor@5.0.0: dependencies: restore-cursor: 5.1.0 @@ -6097,6 +7026,8 @@ snapshots: color-name@1.1.4: {} + commander@12.1.0: {} + commander@14.0.3: {} commander@4.1.1: {} @@ -6161,6 +7092,8 @@ snapshots: entities@4.5.0: {} + es-errors@1.3.0: {} + es-module-lexer@1.7.0: {} esbuild@0.25.12: @@ -6301,6 +7234,8 @@ snapshots: dependencies: fetch-blob: 3.2.0 + forwarded-parse@2.1.2: {} + fs-extra@7.0.1: dependencies: graceful-fs: 4.2.11 @@ -6321,6 +7256,8 @@ snapshots: fsevents@2.3.3: optional: true + function-bind@1.1.2: {} + gensync@1.0.0-beta.2: {} get-caller-file@2.0.5: {} @@ -6362,6 +7299,10 @@ snapshots: has-flag@4.0.0: {} + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + hosted-git-info@7.0.2: dependencies: lru-cache: 10.4.3 @@ -6382,10 +7323,26 @@ snapshots: ignore@5.3.2: {} + import-in-the-middle@1.15.0: + dependencies: + acorn: 8.16.0 + acorn-import-attributes: 1.9.5(acorn@8.16.0) + cjs-module-lexer: 1.4.3 + module-details-from-path: 1.0.4 + imurmurhash@0.1.4: {} index-to-position@1.2.0: {} + ipfs-unixfs@11.2.5: + dependencies: + protons-runtime: 5.6.0 + uint8arraylist: 2.4.9 + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + is-extglob@2.1.1: {} is-fullwidth-code-point@3.0.0: {} @@ -6617,6 +7574,8 @@ snapshots: mock-socket@9.3.1: {} + module-details-from-path@1.0.4: {} + mri@1.2.0: {} ms@2.1.3: {} @@ -6769,6 +7728,8 @@ snapshots: path-key@4.0.0: {} + path-parse@1.0.7: {} + path-scurry@1.11.1: dependencies: lru-cache: 10.4.3 @@ -6780,6 +7741,18 @@ snapshots: pathval@2.0.1: {} + pg-int8@1.0.1: {} + + pg-protocol@1.13.0: {} + + pg-types@2.2.0: + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.1 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + picocolors@1.1.1: {} picomatch@2.3.2: {} @@ -6839,34 +7812,6 @@ snapshots: - utf-8-validate - yaml - polkadot-api@2.0.0(esbuild@0.25.12)(rxjs@7.8.2): - dependencies: - '@polkadot-api/cli': 0.20.1(esbuild@0.25.12) - '@polkadot-api/ink-contracts': 0.6.0 - '@polkadot-api/json-rpc-provider': 0.2.0 - '@polkadot-api/known-chains': 0.11.1 - '@polkadot-api/logs-provider': 0.2.0 - '@polkadot-api/metadata-builders': 0.14.0 - '@polkadot-api/metadata-compatibility': 0.6.0 - '@polkadot-api/observable-client': 0.18.3(rxjs@7.8.2) - '@polkadot-api/pjs-signer': 0.7.0 - '@polkadot-api/polkadot-signer': 0.1.6 - '@polkadot-api/signer': 0.3.0 - '@polkadot-api/sm-provider': 0.3.0(@polkadot-api/smoldot@0.4.0) - '@polkadot-api/smoldot': 0.4.0 - '@polkadot-api/substrate-bindings': 0.20.0 - '@polkadot-api/substrate-client': 0.7.0 - '@polkadot-api/utils': 0.4.0 - '@polkadot-api/ws-middleware': 0.3.0(rxjs@7.8.2) - '@polkadot-api/ws-provider': 0.9.0(rxjs@7.8.2) - '@rx-state/core': 0.1.4(rxjs@7.8.2) - rxjs: 7.8.2 - transitivePeerDependencies: - - bufferutil - - esbuild - - supports-color - - utf-8-validate - polkadot-api@2.0.0(esbuild@0.27.7)(rxjs@7.8.2): dependencies: '@polkadot-api/cli': 0.20.1(esbuild@0.27.7) @@ -6916,6 +7861,16 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + postgres-array@2.0.0: {} + + postgres-bytea@1.0.1: {} + + postgres-date@1.0.7: {} + + postgres-interval@1.2.0: + dependencies: + xtend: 4.0.2 + prettier@2.8.8: {} pretty-ms@9.3.0: @@ -6924,6 +7879,12 @@ snapshots: propagate@2.0.1: {} + protons-runtime@5.6.0: + dependencies: + uint8-varint: 2.0.4 + uint8arraylist: 2.4.9 + uint8arrays: 5.1.1 + punycode.js@2.3.1: {} punycode@2.3.1: {} @@ -6972,12 +7933,27 @@ snapshots: require-directory@2.1.1: {} + require-in-the-middle@7.5.2: + dependencies: + debug: 4.4.3 + module-details-from-path: 1.0.4 + resolve: 1.22.12 + transitivePeerDependencies: + - supports-color + require-main-filename@2.0.0: {} resolve-from@5.0.0: {} resolve-pkg-maps@1.0.0: {} + resolve@1.22.12: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + restore-cursor@5.1.0: dependencies: onetime: 7.0.0 @@ -6985,17 +7961,6 @@ snapshots: reusify@1.1.0: {} - rollup-plugin-esbuild@6.2.1(esbuild@0.25.12)(rollup@4.60.1): - dependencies: - debug: 4.4.3 - es-module-lexer: 1.7.0 - esbuild: 0.25.12 - get-tsconfig: 4.13.7 - rollup: 4.60.1 - unplugin-utils: 0.2.5 - transitivePeerDependencies: - - supports-color - rollup-plugin-esbuild@6.2.1(esbuild@0.27.7)(rollup@4.60.1): dependencies: debug: 4.4.3 @@ -7096,6 +8061,8 @@ snapshots: shebang-regex@3.0.0: {} + shimmer@1.2.1: {} + siginfo@2.0.0: {} signal-exit@4.1.0: {} @@ -7208,6 +8175,8 @@ snapshots: dependencies: has-flag: 4.0.0 + supports-preserve-symlinks-flag@1.0.0: {} + tagged-tag@1.0.0: {} tailwindcss@4.2.2: {} @@ -7336,6 +8305,19 @@ snapshots: ufo@1.6.3: {} + uint8-varint@2.0.4: + dependencies: + uint8arraylist: 2.4.9 + uint8arrays: 5.1.1 + + uint8arraylist@2.4.9: + dependencies: + uint8arrays: 5.1.1 + + uint8arrays@5.1.1: + dependencies: + multiformats: 13.4.2 + undici-types@7.16.0: {} undici-types@7.19.2: {} @@ -7524,6 +8506,8 @@ snapshots: ws@8.20.0: {} + xtend@4.0.2: {} + y18n@4.0.3: {} yallist@3.1.1: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 2d6f2d3..2b2544c 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,5 +1,6 @@ packages: - "packages/*" + - "apps/*" - "examples/*" catalog: @@ -24,3 +25,6 @@ catalog: "@vitest/coverage-istanbul": ^3.1.4 "@polkadot-labs/hdkd": ^0.0.26 "@polkadot-labs/hdkd-helpers": ^0.0.27 + commander: ^12.0.0 + bulletin-deploy: ^0.6.4 + ws: ^8.18.0 diff --git a/scripts/cli-path.sh b/scripts/cli-path.sh new file mode 100755 index 0000000..99e8828 --- /dev/null +++ b/scripts/cli-path.sh @@ -0,0 +1,25 @@ +#!/bin/bash +# Ensures ~/.polkadot/bin is on PATH in the user's shell config. +# Called by `pnpm cli:install` after compiling the dot binary. + +append_once() { + local file="$1" line="$2" + grep -Fqx "$line" "$file" 2>/dev/null || printf "\n%s\n" "$line" >> "$file" +} + +SHELL_NAME=$(basename "$SHELL") + +if [ "$SHELL_NAME" = "zsh" ]; then + append_once "$HOME/.zshrc" 'export PATH="$HOME/.polkadot/bin:$PATH"' + echo "Added ~/.polkadot/bin to PATH in ~/.zshrc" +elif [ "$SHELL_NAME" = "bash" ]; then + append_once "$HOME/.bashrc" 'export PATH="$HOME/.polkadot/bin:$PATH"' + append_once "$HOME/.bash_profile" '[ -f "$HOME/.bashrc" ] && . "$HOME/.bashrc"' + echo "Added ~/.polkadot/bin to PATH in ~/.bashrc" +elif [ "$SHELL_NAME" = "fish" ]; then + mkdir -p "$HOME/.config/fish" + append_once "$HOME/.config/fish/config.fish" 'fish_add_path $HOME/.polkadot/bin' + echo "Added ~/.polkadot/bin to PATH in fish config" +fi + +echo "Restart your shell or run: export PATH=\"\$HOME/.polkadot/bin:\$PATH\"" diff --git a/turbo.json b/turbo.json index 3739e54..df3a87d 100644 --- a/turbo.json +++ b/turbo.json @@ -10,6 +10,10 @@ "dependsOn": ["^build"], "outputs": ["generated/dist/**"] }, + "@polkadot-apps/cli#build": { + "dependsOn": ["^build"], + "outputs": [] + }, "dev": { "cache": false, "persistent": true diff --git a/typedoc.json b/typedoc.json index 03ae2de..35c6ef6 100644 --- a/typedoc.json +++ b/typedoc.json @@ -1,6 +1,6 @@ { "$schema": "https://typedoc.org/schema.json", - "entryPoints": ["packages/*"], + "entryPoints": ["packages/*", "apps/*"], "entryPointStrategy": "packages", "out": "docs", "name": "Polkadot Apps", diff --git a/vitest.config.ts b/vitest.config.ts index a92b1e7..2b36491 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -10,7 +10,7 @@ function ignoreCoverageBlocks(): Plugin { return { name: "ignore-coverage-blocks", transform(code, id) { - if (!id.includes("packages/")) return; + if (!id.includes("packages/") && !id.includes("apps/")) return; let result = code; // Exclude in-source test blocks @@ -37,8 +37,8 @@ export default defineConfig({ plugins: [ignoreCoverageBlocks()], test: { globals: true, - includeSource: ["packages/*/src/**/*.ts"], - include: ["packages/**/tests/**/*.test.ts"], + includeSource: ["packages/*/src/**/*.ts", "apps/*/src/**/*.ts"], + include: ["packages/**/tests/**/*.test.ts", "apps/**/tests/**/*.test.ts"], // Playwright E2E specs under examples/**/e2e live alongside vitest unit // tests in this repo; exclude them so vitest doesn't try to execute // `test.describe` from @playwright/test. @@ -49,7 +49,7 @@ export default defineConfig({ provider: "istanbul", reporter: ["text", "json-summary", "json"], reportsDirectory: "./coverage", - include: ["packages/*/src/**/*.ts"], + include: ["packages/*/src/**/*.ts", "apps/*/src/**/*.ts"], exclude: [ "**/node_modules/**", "**/dist/**", @@ -57,6 +57,8 @@ export default defineConfig({ "**/index.ts", "**/types.ts", "**/encoding.ts", + // CLI is private and compiled via bun — exclude from coverage + "apps/cli/**", // Integration-only: require real WebSocket/provider connections "**/container.ts", "**/providers.ts",