From cc00adce502987f50f4493fbc717c6baf242ff38 Mon Sep 17 00:00:00 2001 From: Dhruv Sharma Date: Fri, 24 Jul 2026 20:39:17 +0530 Subject: [PATCH 1/5] chore(deps): upgrade product-sdk to 0.19 and drop the novasama host-api Bump product-sdk ^0.19.1, chain-client ^0.9.1, contracts ^0.9.2, descriptors ^0.8.0, signer ^0.11.1, tx ^0.3.2; drop @novasamatech/host-api(-wrapper). The two remaining wrapper call sites (requestPermission, preimageManager) move to @parity/product-sdk-host. Result-API migration: contract .tx unwrapped in the getContract proxy, fromLiveClient and ensureContractAccountMapped branch on .ok. Override ws ^8.21.0 to clear GHSA-96hv-2xvq-fx4p pulled in via contracts' viem. Closes #5 --- package.json | 17 +++++++------- src/utils.ts | 64 +++++++++++++++++++++++++++++++++------------------- 2 files changed, 49 insertions(+), 32 deletions(-) diff --git a/package.json b/package.json index 484b6d5..4da3b46 100644 --- a/package.json +++ b/package.json @@ -14,15 +14,13 @@ }, "dependencies": { "@noble/hashes": "^2.2.0", - "@novasamatech/host-api": "^0.8.10", - "@novasamatech/host-api-wrapper": "^0.8.10", - "@parity/product-sdk": "^0.15.1", + "@parity/product-sdk": "^0.19.1", "@parity/product-sdk-address": "^0.1.1", - "@parity/product-sdk-chain-client": "^0.7.5", - "@parity/product-sdk-contracts": "^0.8.1", - "@parity/product-sdk-descriptors": "^0.6.2", - "@parity/product-sdk-signer": "^0.8.2", - "@parity/product-sdk-tx": "^0.2.15", + "@parity/product-sdk-chain-client": "^0.9.1", + "@parity/product-sdk-contracts": "^0.9.2", + "@parity/product-sdk-descriptors": "^0.8.0", + "@parity/product-sdk-signer": "^0.11.1", + "@parity/product-sdk-tx": "^0.3.2", "multiformats": "^13.4.2", "polkadot-api": "^2.1.6", "react": "^19.0.0", @@ -36,6 +34,7 @@ "vite": "^8.0.16" }, "overrides": { - "@polkadot-api/json-rpc-provider": "0.2.0" + "@polkadot-api/json-rpc-provider": "0.2.0", + "ws": "^8.21.0" } } diff --git a/src/utils.ts b/src/utils.ts index d206271..165d932 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,8 +1,8 @@ import { useState, useEffect } from "react"; import { - preimageManager, + getPreimageManager, requestPermission, -} from "@novasamatech/host-api-wrapper"; +} from "@parity/product-sdk-host"; import { SignerManager, HostProvider, @@ -34,11 +34,11 @@ async function ensurePermission(tag: "ChainSubmit" | "PreimageSubmit" | "Stateme if (_grantedPermissions.has(tag)) return; try { const result = await requestPermission({ tag, value: undefined }); - if (result.isOk() && result.value) { + if (result.ok && result.value) { _grantedPermissions.add(tag); console.log(`[Permission] ${tag} granted`); } else { - console.warn(`[Permission] ${tag} denied`, result.isErr() ? result.error : "user rejected"); + console.warn(`[Permission] ${tag} denied`, result.ok ? "user rejected" : result.error); } } catch (err) { console.warn(`[Permission] ${tag} request failed:`, err); @@ -226,6 +226,10 @@ export async function uploadToBulletin(_account: AppAccount, bytes: Uint8Array): await ensurePermission("PreimageSubmit"); const cid = calculateCID(bytes); console.log("[Bulletin] Submitting preimage via host, size:", bytes.length, "expected CID:", cid); + const preimageManager = await getPreimageManager(); + if (!preimageManager) { + throw new Error("Preimage manager unavailable — open this app inside a Polkadot host."); + } await preimageManager.submit(bytes); console.log("[Bulletin] Preimage stored."); return cid; @@ -359,7 +363,9 @@ async function ensureContractsReady(): Promise { // `fromLiveClient` resolves the deployed contract address from the live // CDM registry on each init instead of trusting the snapshot baked into // cdm.json — a redeploy is picked up without shipping a new cdm.json. - _contractManager = await ContractManager.fromLiveClient( + // Since product-sdk 0.18, fromLiveClient returns a Result instead of + // throwing on resolution failure. + const live = await ContractManager.fromLiveClient( _cdmJson, client, paseo_asset_hub, @@ -370,6 +376,8 @@ async function ensureContractsReady(): Promise { libraries: ["@example/feedback"], }, ); + if (!live.ok) throw live.error; + _contractManager = live.value; _contract = wrapContract(_contractManager.getContract("@example/feedback")); console.log("[CDM] Contract manager ready (live registry resolution)"); })(); @@ -393,7 +401,16 @@ export function getContract(): any { if (!_contract) throw new Error("Contract init failed"); const real = _contract[prop as string]; if (!real) throw new Error(`Unknown method: ${String(prop)}`); - return real[methodProp](...args); + // Since product-sdk 0.18, `.tx(...)` returns a Result + // instead of throwing. Unwrap it (re-throw the `err` + // channel) so call sites keep their try/catch flow. + // `.query(...)` is unchanged upstream. + const outcome = await real[methodProp](...args); + if (methodProp === "tx") { + if (!outcome.ok) throw outcome.error; + return outcome.value; + } + return outcome; }; }, }); @@ -429,25 +446,26 @@ async function mapAccountWithRuntime( account: AppAccount, ): Promise { if (_mappedAccounts.has(account.address)) return; - try { - const mapped = await ensureContractAccountMapped( - runtime, - account.address as never, - account.signer, - ); - if (mapped === null) { - console.log(`[Revive] Account ${account.address} already mapped`); - } else { - console.log(`[Revive] Account mapped in block #${mapped.block.number}`); + // Since product-sdk 0.18, ensureContractAccountMapped returns a Result + // (ok(null) = already mapped) instead of throwing. + const mapped = await ensureContractAccountMapped( + runtime, + account.address as never, + account.signer, + ); + if (!mapped.ok) { + console.error("[Revive] ensureContractAccountMapped failed:", mapped.error); + if (mapped.error.cause) { + console.error("[Revive] underlying cause:", mapped.error.cause); } - _mappedAccounts.add(account.address); - } catch (err) { - console.error("[Revive] ensureContractAccountMapped failed:", err); - if (err && typeof err === "object" && "cause" in err) { - console.error("[Revive] underlying cause:", (err as any).cause); - } - throw err; + throw mapped.error; + } + if (mapped.value === null) { + console.log(`[Revive] Account ${account.address} already mapped`); + } else { + console.log(`[Revive] Account mapped in block #${mapped.value.block.number}`); } + _mappedAccounts.add(account.address); } export async function ensureMapping(account: AppAccount): Promise { From 170c7f889dc18cb70bc771e8bf4447c6fd63b86d Mon Sep 17 00:00:00 2001 From: Dhruv Sharma Date: Thu, 30 Jul 2026 15:22:05 +0530 Subject: [PATCH 2/5] fix: declare host directly and harden mapping Declare @parity/product-sdk-host directly and drop the unused @parity/product-sdk umbrella so host no longer resolves only by hoisting. Unwrap the contract .tx Result and wrap the account mapping in try/catch so the cause logging runs whether it returns an error or throws. Update the stale @novasamatech/host-api reference in the AI-config files. --- .clinerules | 2 +- .github/copilot-instructions.md | 2 +- .windsurfrules | 2 +- package.json | 2 +- src/utils.ts | 57 ++++++++++++++++++++------------- 5 files changed, 38 insertions(+), 27 deletions(-) diff --git a/.clinerules b/.clinerules index 34cf2a8..f55f912 100644 --- a/.clinerules +++ b/.clinerules @@ -2,5 +2,5 @@ Feedback Board is a decentralized sticky-note board on Polkadot — notes are stored on Bulletin and indexed by a shared Asset Hub contract, so every remix sees the same board. (React + Vite + TypeScript). Full agent guidance is in `CLAUDE.md` at the repo root — read it before proposing changes. -- Host API (`@parity/product-sdk-signer` + `@novasamatech/host-api`) is how the embedded dapp obtains accounts and requests signatures from the host (Polkadot Mobile, Desktop, or Web) over a postMessage transport; signing is approved on Polkadot Mobile (Desktop/Web relay to the paired phone); only works when embedded. +- Host API (`@parity/product-sdk-signer` + `@parity/product-sdk-host`) is how the embedded dapp obtains accounts and requests signatures from the host (Polkadot Mobile, Desktop, or Web) over a postMessage transport; signing is approved on Polkadot Mobile (Desktop/Web relay to the paired phone); only works when embedded. - No browser/extension fallbacks — out-of-scope on purpose. Always go through `@parity/product-sdk-*` / the Host API; never reach a chain via direct full-node RPC. diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index c100297..f2bc0ee 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -2,5 +2,5 @@ Feedback Board is a decentralized sticky-note board on Polkadot — notes are stored on Bulletin and indexed by a shared Asset Hub contract, so every remix sees the same board. (React + Vite + TypeScript). Full AI agent guidance is in `CLAUDE.md` at the repo root — read it before proposing changes. -- Host API (`@parity/product-sdk-signer` + `@novasamatech/host-api`) is how the embedded dapp obtains accounts and requests signatures from the host (Polkadot Mobile, Desktop, or Web) over a postMessage transport; signing is approved on Polkadot Mobile (Desktop/Web relay to the paired phone); only works when embedded. +- Host API (`@parity/product-sdk-signer` + `@parity/product-sdk-host`) is how the embedded dapp obtains accounts and requests signatures from the host (Polkadot Mobile, Desktop, or Web) over a postMessage transport; signing is approved on Polkadot Mobile (Desktop/Web relay to the paired phone); only works when embedded. - No browser/extension fallbacks — out-of-scope on purpose. Always go through `@parity/product-sdk-*` / the Host API; never reach a chain via direct full-node RPC. diff --git a/.windsurfrules b/.windsurfrules index 34cf2a8..f55f912 100644 --- a/.windsurfrules +++ b/.windsurfrules @@ -2,5 +2,5 @@ Feedback Board is a decentralized sticky-note board on Polkadot — notes are stored on Bulletin and indexed by a shared Asset Hub contract, so every remix sees the same board. (React + Vite + TypeScript). Full agent guidance is in `CLAUDE.md` at the repo root — read it before proposing changes. -- Host API (`@parity/product-sdk-signer` + `@novasamatech/host-api`) is how the embedded dapp obtains accounts and requests signatures from the host (Polkadot Mobile, Desktop, or Web) over a postMessage transport; signing is approved on Polkadot Mobile (Desktop/Web relay to the paired phone); only works when embedded. +- Host API (`@parity/product-sdk-signer` + `@parity/product-sdk-host`) is how the embedded dapp obtains accounts and requests signatures from the host (Polkadot Mobile, Desktop, or Web) over a postMessage transport; signing is approved on Polkadot Mobile (Desktop/Web relay to the paired phone); only works when embedded. - No browser/extension fallbacks — out-of-scope on purpose. Always go through `@parity/product-sdk-*` / the Host API; never reach a chain via direct full-node RPC. diff --git a/package.json b/package.json index 4da3b46..669d959 100644 --- a/package.json +++ b/package.json @@ -14,11 +14,11 @@ }, "dependencies": { "@noble/hashes": "^2.2.0", - "@parity/product-sdk": "^0.19.1", "@parity/product-sdk-address": "^0.1.1", "@parity/product-sdk-chain-client": "^0.9.1", "@parity/product-sdk-contracts": "^0.9.2", "@parity/product-sdk-descriptors": "^0.8.0", + "@parity/product-sdk-host": "^0.14.1", "@parity/product-sdk-signer": "^0.11.1", "@parity/product-sdk-tx": "^0.3.2", "multiformats": "^13.4.2", diff --git a/src/utils.ts b/src/utils.ts index 165d932..83572c7 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -24,6 +24,21 @@ import { CID } from "multiformats/cid"; import * as raw from "multiformats/codecs/raw"; import type { MultihashDigest } from "multiformats/hashes/interface"; +/** + * Unwrap a product-sdk `Result` to its value, re-throwing the `err` channel as + * an `Error`. Since product-sdk 0.18 fallible calls return `Result` instead of + * throwing; this bridges them back onto throw / try-catch control flow. Mirrors + * the CLI's `unwrapResult` (playground-cli #470). + */ +export function unwrapResult( + result: { ok: true; value: T } | { ok: false; error: unknown }, +): T { + if (!result.ok) { + throw result.error instanceof Error ? result.error : new Error(String(result.error)); + } + return result.value; +} + // --------------------------------------------------------------------------- // Permissions (RFC-0002) // --------------------------------------------------------------------------- @@ -406,11 +421,7 @@ export function getContract(): any { // channel) so call sites keep their try/catch flow. // `.query(...)` is unchanged upstream. const outcome = await real[methodProp](...args); - if (methodProp === "tx") { - if (!outcome.ok) throw outcome.error; - return outcome.value; - } - return outcome; + return methodProp === "tx" ? unwrapResult(outcome) : outcome; }; }, }); @@ -446,26 +457,26 @@ async function mapAccountWithRuntime( account: AppAccount, ): Promise { if (_mappedAccounts.has(account.address)) return; - // Since product-sdk 0.18, ensureContractAccountMapped returns a Result - // (ok(null) = already mapped) instead of throwing. - const mapped = await ensureContractAccountMapped( - runtime, - account.address as never, - account.signer, - ); - if (!mapped.ok) { - console.error("[Revive] ensureContractAccountMapped failed:", mapped.error); - if (mapped.error.cause) { - console.error("[Revive] underlying cause:", mapped.error.cause); + try { + // Since product-sdk 0.18, ensureContractAccountMapped returns a Result + // (ok(null) = already mapped) instead of throwing. Unwrap it so the + // catch handles both a returned `err` and any thrown failure with the + // same cause-chain logging. + const mapped = unwrapResult( + await ensureContractAccountMapped(runtime, account.address as never, account.signer), + ); + if (mapped === null) { + console.log(`[Revive] Account ${account.address} already mapped`); + } else { + console.log(`[Revive] Account mapped in block #${mapped.block.number}`); } - throw mapped.error; - } - if (mapped.value === null) { - console.log(`[Revive] Account ${account.address} already mapped`); - } else { - console.log(`[Revive] Account mapped in block #${mapped.value.block.number}`); + _mappedAccounts.add(account.address); + } catch (err) { + console.error("[Revive] ensureContractAccountMapped failed:", err); + const cause = err && typeof err === "object" ? (err as { cause?: unknown }).cause : undefined; + if (cause) console.error("[Revive] underlying cause:", cause); + throw err; } - _mappedAccounts.add(account.address); } export async function ensureMapping(account: AppAccount): Promise { From c9da8ce0322879ea9b67051c3eb94eede2854268 Mon Sep 17 00:00:00 2001 From: Dhruv Sharma Date: Fri, 24 Jul 2026 20:40:15 +0530 Subject: [PATCH 3/5] feat: add devnet as a selectable network Two-network config (paseo-next, devnet) selected at build time via VITE_NETWORK; default stays paseo-next. Descriptors load dynamically and the host-routed chain client takes its identity from the loaded descriptor. fromLiveClient now takes the selected network's registry address, so devnet resolves the contract against the devnet registry. Add the cdm devnet preset deploy script and a paseo-next vs devnet note in DEPLOYMENT.md. Closes #6 --- DEPLOYMENT.md | 20 ++++++++++++ package.json | 1 + src/utils.ts | 84 +++++++++++++++++++++++++++++++++++++++++++-------- 3 files changed, 93 insertions(+), 12 deletions(-) diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index 4188c87..b8a1c4c 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -167,6 +167,26 @@ cdm deploy -n paseo (This repo also ships `npm run deploy`, which runs the same command with the network endpoints pinned explicitly. Either works.) +> **Which network is which:** the CDM `paseo` preset targets **Paseo Next v2** +> (para 1500, a preview network), not the public Paseo testnet. To target +> **devnet** — the public products devnet on the Paseo testnet Asset Hub +> (para 1000, community-operated CDM registry) — use `cdm deploy -n devnet` +> (or `npm run deploy:devnet`; endpoints and registry come from the preset), +> and build the frontend with `VITE_NETWORK=devnet` so it connects to devnet +> and resolves the contract from the devnet registry. +> +> Deploying to devnet, the whole flow uses `-n devnet` in place of `-n paseo` +> (`cdm account map -n devnet`, `cdm account bal -n devnet`, `cdm i -n devnet`), +> and the PAS faucet is the **para-1000** one: +> . The `-n devnet` preset needs +> **CDM v0.9.0+** (`cdm --version`); older CLIs report `unknown preset`. +> +> **Deploy the contract to devnet before shipping a `VITE_NETWORK=devnet` +> build.** The frontend resolves the contract address from the devnet registry +> at startup and there is no `cdm.json` fallback, so a devnet build pointed at +> a network where `@example/feedback` isn't registered fails at contract init +> with a resolution error. + *What's happening, in order:* (1) rebuilds if needed, (2) deploys the bytecode to Paseo Asset Hub via pallet-revive, (3) publishes the contract metadata to Bulletin, (4) registers `@/feedback → (address, diff --git a/package.json b/package.json index 669d959..aec92d2 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "build:frontend": "tsc -b && vite build", "build:contracts": "cdm build", "deploy": "cdm deploy -n paseo --registry-address 0xf62c2ece29cd8df2e10040ecfa5a894a5c5d9cb0 --assethub-url wss://paseo-asset-hub-next-rpc.polkadot.io --bulletin-url wss://paseo-bulletin-next-rpc.polkadot.io", + "deploy:devnet": "cdm deploy -n devnet", "preview": "vite preview" }, "dependencies": { diff --git a/src/utils.ts b/src/utils.ts index 83572c7..803bec4 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -17,7 +17,8 @@ import { createContractRuntimeFromClient, ensureContractAccountMapped, } from "@parity/product-sdk-contracts"; -import { paseo_asset_hub } from "@parity/product-sdk-descriptors/paseo-asset-hub"; +import type { devnet_asset_hub } from "@parity/product-sdk-descriptors/devnet-asset-hub"; +import type { paseo_asset_hub } from "@parity/product-sdk-descriptors/paseo-asset-hub"; import type { PolkadotClient, PolkadotSigner } from "polkadot-api"; import { blake2b } from "@noble/hashes/blake2.js"; import { CID } from "multiformats/cid"; @@ -39,6 +40,67 @@ export function unwrapResult( return result.value; } +// --------------------------------------------------------------------------- +// Networks. "paseo-next" is the Paseo Next v2 preview network (para 1500) — +// that's what the CDM `paseo` preset targets, NOT the public Paseo testnet. +// "devnet" is the public products devnet on the Paseo testnet Asset Hub +// (para 1000), with the community-operated CDM registry (reference: +// contract-dependency-manager PR #61). Build with VITE_NETWORK=devnet to +// select it; the default stays paseo-next. Descriptors load dynamically so +// each build ships a single metadata chunk; chain identity comes from the +// descriptor (host-routed chain client, no endpoints or genesis to pin). +// --------------------------------------------------------------------------- + +type AssetHubDescriptor = typeof devnet_asset_hub | typeof paseo_asset_hub; + +interface NetworkConfig { + label: string; + /** IPFS gateways used to read Bulletin content, most specific first. */ + gateways: readonly string[]; + /** CDM ContractRegistry address the contract resolves against. */ + registry: string; + loadDescriptor(): Promise; +} + +const NETWORKS: Record<"paseo-next" | "devnet", NetworkConfig> = { + "paseo-next": { + label: "Paseo Next", + gateways: [ + "https://paseo-bulletin-next-ipfs.polkadot.io/ipfs/", + "https://dweb.link/ipfs/", + "https://ipfs.io/ipfs/", + "https://nftstorage.link/ipfs/", + ], + registry: "0xf62c2ece29cd8df2e10040ecfa5a894a5c5d9cb0", + loadDescriptor: async () => + (await import("@parity/product-sdk-descriptors/paseo-asset-hub")).paseo_asset_hub, + }, + // Bulletin Paseo has no dedicated HTTP gateway; devnet content is read + // via public IPFS gateways. + devnet: { + label: "Devnet (Paseo testnet)", + gateways: [ + "https://ipfs.io/ipfs/", + "https://dweb.link/ipfs/", + "https://nftstorage.link/ipfs/", + ], + registry: "0x59b0245778917af55224e5f8fb55f7f8d452619f", + loadDescriptor: async () => + (await import("@parity/product-sdk-descriptors/devnet-asset-hub")).devnet_asset_hub, + }, +}; + +function resolveNetwork(): NetworkConfig { + const selected = (import.meta.env.VITE_NETWORK ?? "").trim().toLowerCase(); + if (selected === "devnet") return NETWORKS.devnet; + if (selected && selected !== "paseo" && selected !== "paseo-next") { + console.warn(`[Network] Unknown VITE_NETWORK "${selected}", falling back to paseo-next`); + } + return NETWORKS["paseo-next"]; +} + +export const NETWORK = resolveNetwork(); + // --------------------------------------------------------------------------- // Permissions (RFC-0002) // --------------------------------------------------------------------------- @@ -348,12 +410,13 @@ async function ensureContractsReady(): Promise { // so the host never prompts "Allow Access to Web Domains" for a raw RPC // endpoint, and the chain identity comes from the descriptor — no // hardcoded genesis. + const descriptor = await NETWORK.loadDescriptor(); const chainClient = await createChainClient({ - chains: { assetHub: paseo_asset_hub }, + chains: { assetHub: descriptor }, }); const client = chainClient.raw.assetHub; _polkadotClient = client; - console.log("[CDM] Asset Hub chain client ready (host-routed)"); + console.log(`[CDM] Asset Hub chain client ready (host-routed, ${NETWORK.label})`); console.log("[CDM] Waking Asset Hub chain follow..."); await client.getChainSpecData(); @@ -372,21 +435,23 @@ async function ensureContractsReady(): Promise { // and pallet-revive dry-run-fails that call with `Revive::AccountUnmapped` // when the query origin isn't mapped. Build a plain runtime (no registry // query) to perform the mapping first. - const initRuntime = createContractRuntimeFromClient(client, paseo_asset_hub); + const initRuntime = createContractRuntimeFromClient(client, descriptor); await mapAccountWithRuntime(initRuntime, _state.account); // `fromLiveClient` resolves the deployed contract address from the live // CDM registry on each init instead of trusting the snapshot baked into // cdm.json — a redeploy is picked up without shipping a new cdm.json. // Since product-sdk 0.18, fromLiveClient returns a Result instead of - // throwing on resolution failure. + // throwing on resolution failure. The registry address comes from the + // selected network, so devnet resolves against the devnet registry. const live = await ContractManager.fromLiveClient( _cdmJson, client, - paseo_asset_hub, + descriptor, { defaultOrigin: _state.account.address as never, defaultSigner: _state.account.signer, + registryAddress: NETWORK.registry as never, registryOrigin: _state.account.address as never, libraries: ["@example/feedback"], }, @@ -490,12 +555,7 @@ export async function ensureMapping(account: AppAccount): Promise { // Bulletin reads via public IPFS gateways (Promise.any race). // --------------------------------------------------------------------------- -const GATEWAYS = [ - "https://paseo-bulletin-next-ipfs.polkadot.io/ipfs/", - "https://dweb.link/ipfs/", - "https://ipfs.io/ipfs/", - "https://nftstorage.link/ipfs/", -] as const; +const GATEWAYS = NETWORK.gateways; export const IPFS_GATEWAY = GATEWAYS[0]; From f6ff9156a9e4b4ec28600380e7ea0ff928ee127f Mon Sep 17 00:00:00 2001 From: Dhruv Sharma Date: Mon, 3 Aug 2026 16:40:38 +0530 Subject: [PATCH 4/5] fix: fold network selection to one metadata chunk --- DEPLOYMENT.md | 6 +++++ src/utils.ts | 74 ++++++++++++++++++++++++++------------------------- 2 files changed, 44 insertions(+), 36 deletions(-) diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index b8a1c4c..363aeb8 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -186,6 +186,12 @@ the network endpoints pinned explicitly. Either works.) > at startup and there is no `cdm.json` fallback, so a devnet build pointed at > a network where `@example/feedback` isn't registered fails at contract init > with a resolution error. +> +> `cdm.json` holds one network's registry, address, version and ABI, and +> `cdm deploy` rewrites them for whichever network you pass. Deploy to a single +> target and commit that manifest; the frontend passes the selected network's +> registry explicitly and re-resolves addresses live, so it tolerates a +> manifest committed for the other network without a code change. *What's happening, in order:* (1) rebuilds if needed, (2) deploys the bytecode to Paseo Asset Hub via pallet-revive, (3) publishes the contract diff --git a/src/utils.ts b/src/utils.ts index 803bec4..2bc5686 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -62,45 +62,47 @@ interface NetworkConfig { loadDescriptor(): Promise; } -const NETWORKS: Record<"paseo-next" | "devnet", NetworkConfig> = { - "paseo-next": { - label: "Paseo Next", - gateways: [ - "https://paseo-bulletin-next-ipfs.polkadot.io/ipfs/", - "https://dweb.link/ipfs/", - "https://ipfs.io/ipfs/", - "https://nftstorage.link/ipfs/", - ], - registry: "0xf62c2ece29cd8df2e10040ecfa5a894a5c5d9cb0", - loadDescriptor: async () => - (await import("@parity/product-sdk-descriptors/paseo-asset-hub")).paseo_asset_hub, - }, - // Bulletin Paseo has no dedicated HTTP gateway; devnet content is read - // via public IPFS gateways. - devnet: { - label: "Devnet (Paseo testnet)", - gateways: [ - "https://ipfs.io/ipfs/", - "https://dweb.link/ipfs/", - "https://nftstorage.link/ipfs/", - ], - registry: "0x59b0245778917af55224e5f8fb55f7f8d452619f", - loadDescriptor: async () => - (await import("@parity/product-sdk-descriptors/devnet-asset-hub")).devnet_asset_hub, - }, -}; - -function resolveNetwork(): NetworkConfig { - const selected = (import.meta.env.VITE_NETWORK ?? "").trim().toLowerCase(); - if (selected === "devnet") return NETWORKS.devnet; - if (selected && selected !== "paseo" && selected !== "paseo-next") { - console.warn(`[Network] Unknown VITE_NETWORK "${selected}", falling back to paseo-next`); +// `import.meta.env.VITE_NETWORK` is inlined as a literal at build time, so this +// comparison folds and the unselected network's ~880 kB metadata chunk is +// dropped from the bundle. Keep it a direct literal comparison — routing the +// choice through a lookup table or a normalizing helper leaves both import()s +// reachable, so the build emits both metadata chunks. +export const NETWORK: NetworkConfig = + import.meta.env.VITE_NETWORK === "devnet" + ? { + label: "Devnet (Paseo testnet)", + // Bulletin Paseo has no dedicated HTTP gateway; read via public IPFS gateways. + gateways: [ + "https://ipfs.io/ipfs/", + "https://dweb.link/ipfs/", + "https://nftstorage.link/ipfs/", + ], + registry: "0x59b0245778917af55224e5f8fb55f7f8d452619f", + loadDescriptor: async () => + (await import("@parity/product-sdk-descriptors/devnet-asset-hub")).devnet_asset_hub, + } + : { + label: "Paseo Next", + gateways: [ + "https://paseo-bulletin-next-ipfs.polkadot.io/ipfs/", + "https://dweb.link/ipfs/", + "https://ipfs.io/ipfs/", + "https://nftstorage.link/ipfs/", + ], + registry: "0xf62c2ece29cd8df2e10040ecfa5a894a5c5d9cb0", + loadDescriptor: async () => + (await import("@parity/product-sdk-descriptors/paseo-asset-hub")).paseo_asset_hub, + }; + +// Dev only: a typo'd VITE_NETWORK silently falls back to paseo-next, so say so +// while developing. Kept out of the selection above to preserve the fold. +if (import.meta.env.DEV) { + const raw = import.meta.env.VITE_NETWORK; + if (raw && raw !== "devnet" && raw !== "paseo" && raw !== "paseo-next") { + console.warn(`[Network] Unknown VITE_NETWORK "${raw}", falling back to paseo-next`); } - return NETWORKS["paseo-next"]; } -export const NETWORK = resolveNetwork(); - // --------------------------------------------------------------------------- // Permissions (RFC-0002) // --------------------------------------------------------------------------- From ec5066d3a03808bee705c91d24b8a2a1a49a8eca Mon Sep 17 00:00:00 2001 From: Dhruv Sharma Date: Tue, 4 Aug 2026 12:05:24 +0530 Subject: [PATCH 5/5] fix: drop the raw import shadow and make the AccountUnmapped doc network-aware --- DEPLOYMENT.md | 2 +- src/utils.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index 363aeb8..4e214b4 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -316,7 +316,7 @@ All of these were hit for real while writing this guide. | Symptom | Cause / fix | |---|---| | Build fails asking for `rustup component add rust-src` | run exactly that, then retry `cdm build` | -| `AccountUnmapped` on deploy or contract call | run `cdm account map -n paseo` (needs a funded account) | +| `AccountUnmapped` on deploy or contract call | run `cdm account map -n ` for the network you deploy to (`-n paseo` for paseo-next, `-n devnet` for devnet; needs a funded account) | | `store data: InvalidTxError {"Invalid":{"Payment"}}` at the end of `cdm deploy` | no Bulletin storage allowance; use the Bulletin faucet from step 2, then re-run the deploy | | Deploy fails with a registry/name conflict | the package name in `Cargo.toml` still belongs to someone else; see step 3 | | `cdm deploy` looks finished but doesn't exit | it's finalizing the registry update; give it a couple of minutes. Don't kill it: an interrupted run leaves the registry on your previous version | diff --git a/src/utils.ts b/src/utils.ts index 2bc5686..58c9dd2 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -97,9 +97,9 @@ export const NETWORK: NetworkConfig = // Dev only: a typo'd VITE_NETWORK silently falls back to paseo-next, so say so // while developing. Kept out of the selection above to preserve the fold. if (import.meta.env.DEV) { - const raw = import.meta.env.VITE_NETWORK; - if (raw && raw !== "devnet" && raw !== "paseo" && raw !== "paseo-next") { - console.warn(`[Network] Unknown VITE_NETWORK "${raw}", falling back to paseo-next`); + const configured = import.meta.env.VITE_NETWORK; + if (configured && configured !== "devnet" && configured !== "paseo" && configured !== "paseo-next") { + console.warn(`[Network] Unknown VITE_NETWORK "${configured}", falling back to paseo-next`); } }