Skip to content

Prove the Product DevNet endpoint set - #114

Merged
knzeng-e merged 15 commits into
devfrom
feat/product-devnet-endpoint-truth
Aug 3, 2026
Merged

Prove the Product DevNet endpoint set#114
knzeng-e merged 15 commits into
devfrom
feat/product-devnet-endpoint-truth

Conversation

@knzeng-e

Copy link
Copy Markdown
Owner

Outcome

Adds npm run smoke:devnet, a read-only check that turns the central claim of
this whole track into evidence: the Product DevNet build profile points at a
chain that actually holds Dotify's contracts.

Stacked on #113. Refs #85.

Issue and context

The roadmap assumed porting to Product DevNet would require deploying Dotify's
contracts onto a DevNet Asset Hub. Investigating that assumption is what this
branch closes out.

Product DevNet is not a separate chain. It is a preset over the Paseo system
parachains - Asset Hub (1000), People (1004), Bulletin (1010) - at EVM chain
420420417. Dotify has been deployed on that chain since the original rollout,
so deployments.json already contains DevNet addresses.

Porting to DevNet is therefore a configuration question, not a migration. This
PR makes the configuration verifiable rather than asserted.

What the check does

cd web && npm run smoke:devnet

Reads web/.env.product-devnet and deployments.json, then verifies read-only:

Check Why it matters
eth_chainId is 420420417 Catches a build pointed at Asset Hub Next (1500), a different network
Head block is past 10612201 That block is where the chain stalled on 2026-07-01; a head at or below it means frozen, not healthy
ArtistDirectory has bytecode The contract every catalog load reads
ArtistRuntimeFactory has bytecode The contract artist onboarding reads
Bulletin RPC responds Publication target
IPFS gateway responds Audio delivery path

It sends no transaction, reads no secret, and prints no credential. It is
network-dependent, so it stays out of npm run test:unit and is wired into the
runbook and CLAUDE.md instead.

Live result

Dotify Product DevNet endpoint smoke
Asset Hub RPC: https://eth-rpc-testnet.polkadot.io/

ok   - Asset Hub reports EVM chain 420420417 (got 420420417)
ok   - Asset Hub is producing blocks past the 2026-07 halt (head 11546553)
ok   - ArtistDirectory is deployed at 0xcf1534c6e2b0e43b9436c1e86a076466dc0f2108 (3660 chars of bytecode)
ok   - ArtistRuntimeFactory is deployed at 0xbd1a11cfce8b5ef7a37e507bc5109895f8f42a72 (20000 chars of bytecode)
ok   - Bulletin RPC responds (HTTP 405)
ok   - IPFS gateway responds (HTTP 404)

Dotify DevNet endpoint smoke passed (6 checks).

HTTP 405 is the correct answer from a Substrate WS RPC to a plain GET; 404 is
an IPFS gateway with no path. Both prove the endpoint is serving.

Configuration changes

The build profile needed no endpoint changes - the configured Bulletin
(wss://bulletin-paseo.tservices.es:8443) and IPFS gateway
(https://devnet-ipfs.api.polkadotcommunity.foundation) were already the ones
the DevNet reference lists. What it needed was honest comments.

The previous note described the Asset Hub endpoint as a "Product
DevNet-compatible" stopgap held "while the write adapter is ported". That framed
a permanent, correct arrangement as temporary. It now says plainly that this is
the DevNet chain, records that
https://paseo-assethub-rpc.laissez-faire.trade serves the identical chain
(same id, same bytecode) and why the Parity-operated endpoint stays primary, and
warns against Asset Hub Next (1500) / People Next (1502).

Also adds https://bulletin-kubo.tservices.es:9443 as a second DevNet IPFS read
gateway, verified reachable.

Review guide

  1. web/scripts/devnet-endpoint-smoke.mjs - check the assertions are the right
    ones and that nothing sensitive is printed.
  2. web/.env.product-devnet - the comments are the substance here.
  3. docs/operations/product-devnet-deployment.md - the new "Contracts Need No
    Redeploy" section replaces an implied migration step.

Verify carefully

  • The smoke check fails loudly on a wrong chain rather than warning.
  • No credential, secret, or private endpoint appears in its output.
  • The head-block threshold still makes sense if the chain halts again.
  • .env.product-devnet still contains only public values.

Validation

Evidence Result
cd web && npm run smoke:devnet 6/6 pass against the live chain, head 11546553
cd web && npm run test:unit 216 pass
cd web && npm run lint 0 errors; the 3 pre-existing App.tsx/ArtistShell.tsx warnings remain
cd web && npm run build pass - 4.4 MB
cd web && npm run build:product-devnet pass - 4.4 MB
cd web && npm run smoke:production-env pass
cd services/api && npm run typecheck && npm test pass, 92 tests
node scripts/backlog-sync.mjs --check --offline pass; pre-existing warnings remain
git diff --check clean

Known limitations and follow-ups

The check proves the chain and endpoints are right. It does not prove the
Product host connects that chain - only a run inside the container can, and
verifyDeployment() in #113 is what reports it when it does not.

Still outstanding for the Product contract path: pallet-revive account mapping
and host-signed transaction evidence, before writes can move off the EVM wallet
path. Reads are wired and gated; writes are deliberately not.

The head-block threshold is a fixed constant. If the chain halts again well past
it, the check would pass on a frozen chain - a freshness window would be
stricter, at the cost of failing during normal RPC lag.

knzeng-e and others added 15 commits July 26, 2026 21:22
The Host signRaw wire format is not pinned by the SDK: HostSignPayloadResponse
carries an untagged signature, and a Substrate host may sign a raw payload
verbatim or inside the conventional <Bytes> envelope. Verification assumed one
shape, so a wrong guess would have failed every Product key request with an
error indistinguishable from a wrong signer.

Accept a bounded set instead: the canonical message verbatim or <Bytes>-wrapped,
and a bare 64-byte or MultiSignature-tagged 65-byte sr25519 signature. Every
variant carries the identical domain-bound message, so this adds no replay,
cross-app, cross-chain, or cross-track surface; a non-sr25519 tag still fails
closed. Route schemas widen to 128 or 130 hex so the tag is checked by the
verifier rather than rejected before it.

Reject EVM-derived account ids for product-sr25519-v1. A 20-byte H160 padded
with 0xee derives back to the H160 it contains, so accepting that shape let a
caller name any paying EVM listener as the requester and rested the boundary on
the curve check alone. A real Product account is a native AccountId32.

A key that parses and derives to the requester but verifies under no variant now
returns PRODUCT_SIGNATURE_REJECTED, kept distinct from SIGNATURE_INVALID so an
envelope problem is separable from a wrong-account problem in logs.

Pin @scure/sr25519 exactly, matching @noble/hashes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Wiring the Product key signatures changed the behaviour but three places still
described the old one. The wallet modal told Product-host users that protected
playback required an EVM signer, the runbook asked operators to confirm no key
is released through the Product identity, and the architecture matrix said the
shipped UI used EIP-191 or a session token - contradicted by its own prose two
sections later.

All three now describe what ships: a connected Product account requests
protected keys through product-sr25519-v1, and paid access plus artist
publishing remain on the EVM signer. This matters beyond tidiness - the stale
runbook step would have had an operator sign off on a denial as correct
behaviour, hiding a real signing failure.

Record the signing envelope decision and the EVM-derived key rejection, and turn
the runbook step into an evidence capture that names which envelope the live
host actually produced.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Track and split counts come from contract storage and the directory enumerates
runtimes Dotify does not control, so Array.from({ length: Number(count) })
allocated before anything could reject a malformed or hostile value. Both
adapters now validate counts first and throw rather than truncate, since a
silent cap would present a partial catalog as complete. The catalog loader
already isolates per-runtime failures, so one bad runtime degrades to a missing
artist.

Mark the two unverified spots in the CDM adapter that must be settled before it
can be selected: waitForTransaction returns immediately where the viem writer
awaits a receipt, and the payForAccess value-transfer shape is inferred rather
than confirmed against generated contract types.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Dotify has no CDM-registered packages and no `cdm install`, but it does not
need them. Its Solidity contracts are deployed through Asset Hub's eth-rpc,
which is a compatibility layer over pallet-revive - the same pallet the Product
SDK contract helpers target - so the deployed H160 addresses are already
reachable without PolkaVM recompilation or a registry entry.

CdmJsonContract needs only version, address, and abi for getContract(), and
`new ContractManager(...)` is documented as snapshot-only. The generator emits
exactly that snapshot from the same Hardhat artifacts the viem bindings come
from, so the two adapters cannot disagree about an ABI.

Artist runtimes are deliberately absent from the manifest: a diamond is
deployed per artist, so its address is known at call time, not build time, and
a placeholder would misrepresent the deployment. Their merged facet ABI is
emitted separately and bound to an address by createContract.

The generator lives in web/ because it needs the SDK's codegen, which is a
frontend dependency; adding the Product SDK tree to contracts/evm just to emit
types would be a worse trade. Unnamed Solidity getter params are named
positionally for codegen only - generateContractTypes interpolates the name
into a tuple label and emits `args: [: HexString]`, which does not parse. The
manifest ABI stays byte-faithful to the artifact.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Implements the contract resolver the CDM adapter was missing. Fixed-address
contracts resolve from the generated manifest through ContractManager;
per-artist runtime diamonds bind the merged facet ABI to their call-time
address through createContract.

Two SDK constraints shape this. First, createChainClient routes exclusively
through the Product host provider with no direct-WebSocket fallback, so Product
mode cannot work in a standalone build - validateProductionEnvironment now
rejects product-cdm unless the host mode is enabled. Second, the host decides
which chain an environment resolves to, and Dotify's runtimes live on Polkadot
Hub TestNet, reached through the `paseo` preset rather than `devnet`.
verifyDeployment queries the directory before any catalog read so a wrong-chain
connection fails with a named error instead of looking like artists with no
releases.

Selection is build-time rather than runtime, for two reasons. Switching the
authority for access policy is a deployment decision made with evidence, not
something a page should flip. And Vite inlines the value, so a viem build
tree-shakes the whole Product graph away: 4.4 MB against 10 MB when opted in.
The difference is @parity/product-sdk-descriptors, whose shared descriptors
module references every chain's metadata - only one chunk is ever fetched, but
all are published, and Bulletin storage is a finite quota. Both shipped builds
stay at 4.4 MB.

Reads only. Writes stay on the viem signer path in every mode, because routing
a payment or a publication through a signer with no host transaction evidence
is not a reasonable default. A failed Product setup rejects every read rather
than falling back to viem: the adapter in use must never be ambiguous.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The blocking unknown for Product contract mode is no longer the manifest or
generated types - both now exist and are wired. It is whether the Product host
serves a chain that holds Dotify's runtimes, since the host controls that
mapping and the contracts are on Polkadot Hub TestNet rather than Product
DevNet Asset Hub.

Also records the measured build-size trade-off, so an operator weighs it
against the Bulletin quota before enabling product-cdm for a .dot deployment.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The previous default was wrong in a way that would have produced an empty
catalog. Product DevNet is not a separate network: it is a preset over the
Paseo system parachains - Asset Hub (1000), People (1004), Bulletin (1010) -
at EVM chain 420420417. That is exactly where Dotify is already deployed.

Verified read-only against both endpoints for the ArtistDirectory at
0xcf1534c6e2b0e43b9436c1e86a076466dc0f2108: eth-rpc-testnet.polkadot.io and
paseo-assethub-rpc.laissez-faire.trade both report chain id 0x190f1b41, blocks
one apart, and byte-identical contract code. They are two providers for one
chain, so no contract redeploy is needed to port Dotify to DevNet.

The SDK's `paseo` preset is the trap: it targets Paseo Next (Asset Hub Next
1500 / People Next 1502), which the Product docs call a different network where
"funds sent there will not appear on this Devnet". Dotify has no deployment
there, so ProductChainEnvironment now admits only `devnet` - selecting a chain
that cannot hold the catalog is a bug, not a configuration option.

Also realigns the environment reference tables, clearing the markdownlint
MD060 warnings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Porting Dotify to Product DevNet turned out to be a configuration question
rather than a migration: DevNet is a preset over the Paseo system parachains
(Asset Hub 1000, People 1004, Bulletin 1010) at EVM chain 420420417, which is
where Dotify's contracts already are. This makes that claim checkable instead
of asserted.

`npm run smoke:devnet` reads web/.env.product-devnet and deployments.json and
verifies, read-only, that the configured Asset Hub reports chain 420420417, is
producing blocks past the 2026-07 halt, still serves bytecode for the
ArtistDirectory and ArtistRuntimeFactory, and that the Bulletin RPC and IPFS
gateway respond. It sends no transaction, reads no secret, and prints no
credential. Network-dependent, so it stays out of the unit test path.

The build profile needed no endpoint changes - the configured Bulletin and IPFS
gateway were already the DevNet ones. What it needed was honest comments: the
previous note framed the Asset Hub endpoint as a DevNet-compatible stopgap when
it is in fact the DevNet chain. Adds the second DevNet IPFS gateway as a read
fallback, and a warning against Asset Hub Next (1500) and People Next (1502),
which are a different network holding none of Dotify's contracts.

Verified: 6/6 checks pass against the live chain at head 11546553.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The known-limits bullet still said Product contract mode was blocked on
CDM-installed packages and generated contract types. Both now exist, so the
list overstated what is missing. Only pallet-revive account mapping and
host-signed transaction evidence remain.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@knzeng-e
knzeng-e changed the base branch from feat/product-cdm-manifest to dev August 3, 2026 13:33
@knzeng-e
knzeng-e marked this pull request as ready for review August 3, 2026 13:33
@knzeng-e
knzeng-e merged commit 565a925 into dev Aug 3, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant