Skip to content

Register @dotify/* in the CDM registry without redeploying - #118

Merged
knzeng-e merged 29 commits into
devfrom
feat/cdm-publish-dotify-names
Aug 3, 2026
Merged

Register @dotify/* in the CDM registry without redeploying#118
knzeng-e merged 29 commits into
devfrom
feat/cdm-publish-dotify-names

Conversation

@knzeng-e

@knzeng-e knzeng-e commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Outcome

Adds a cdm:publish task that registers Dotify's already-deployed contracts under @dotify/* in the Product CDM registry. Step 2 of the sequence in #116.

Read-only by default. This PR claims no names — execution needs --confirm and a key.

Branches from main, independent of #117 and the #108#115 stack.

Issue and context

cdm deploy builds, deploys, publishes metadata, and registers in one pass. Dotify cannot use it: the contracts are already deployed and already hold the live catalog, so deploying again would mint new addresses and orphan every existing artist runtime. There is no cdm register command.

Reading the registry contract showed the needed operation is already exposed. publishLatest(name, address, metadata_uri) binds a name to an arbitrary address, and the contract states the rule itself:

The caller only has permission to publish a new version of contract_name if either the name is available or they are already the owner of the name.

So a free name is claimable directly, with no deployment involved. metadata_uri is stored verbatim and never validated — it is a pointer, not a checked reference.

Verified live before building

Against the devnet registry 0x59b0245778917af55224e5f8fb55f7f8d452619f on chain 420420417:

Check Result
Registry deployed 143,280 chars of bytecode
@dotify/artist-directory free (isSome=false)
@dotify/artist-runtime-factory free
@dotify/smart-runtime free
Both target addresses carry bytecode

Live dry run output:

CDM registry: 0x59b0245778917af55224E5f8fB55F7F8D452619f
Chain:        420420417
Publisher:    (not supplied — read-only plan)

@dotify/artist-directory
  address: 0xcf1534C6e2B0E43B9436c1e86A076466dC0F2108
  action:  register
  detail:  name is free; first publisher becomes its permanent owner
  calldata: 0x7dd8b240…

@dotify/artist-runtime-factory
  address: 0xBd1a11cFcE8B5Ef7a37E507bC5109895F8F42a72
  action:  register
  detail:  name is free; first publisher becomes its permanent owner
  calldata: 0x7dd8b240…

Dry run. 2 name(s) would be published.

Safety design

Claiming a name is permanent — first-writer-owns, and the registry exposes no release or transfer entry point. So the task is built to make an accident hard:

  • read-only unless --confirm and --private-key are both given (verified: --confirm alone errors);
  • refuses a target address with no bytecode on the connected chain — publishing a name that points at nothing is worse than not publishing it;
  • refuses a name already owned by another account;
  • refuses a registry address with no code (verified);
  • prints exact calldata in dry run, so the transaction can be inspected or submitted by other means.

@dotify/smart-runtime is deliberately not registered

Artist runtimes are per-artist diamonds with no single address. Publishing one artist's runtime under a shared name would misrepresent the catalog. The name is left unclaimed rather than pointed somewhere convenient.

Two findings worth knowing

CDM's docs independently confirm the preset distinction this project already encodes. From the CDM README: "the paseo preset targets paseo-next … para 1500 — not the Paseo testnet. The devnet preset targets the Paseo testnet Asset Hub (para 1000, EVM chain id 420420417)." That is exactly the correction made in #113. Publishing against the paseo registry would register Dotify's names on a network where its contracts do not exist.

CDM supports Solidity. Via a /// @custom:cdm @org/name NatSpec tag, plus first-pass Hardhat and Foundry templates. The architecture page mentions only PolkaVM bytecode, so this is easy to miss — it means Dotify's existing toolchain is not an obstacle to CDM participation, and the ink! rewrite ruled out in #116 stays ruled out.

Review guide

  1. contracts/evm/tasks/cdmPublish.ts — the plan builder and the refusal conditions.
  2. The architecture doc section — the registry rule and preset trap.

Verify carefully

  • @dotify/* is the namespace you want, permanently.
  • Leaving @dotify/smart-runtime unclaimed is right (someone else could take it).
  • The refusal conditions cover the ways this could go wrong.
  • metadata_uri empty by default is acceptable, or should Bulletin metadata land first.

Validation

Evidence Result
npx hardhat cdm:publish --network polkadotTestnet dry run succeeds against the live registry
guard: bad registry address errors as designed
guard: --confirm without key errors as designed
cd contracts/evm && npm test 53 passing
cd contracts/evm && npm run compile pass
cd contracts/evm && npm run fmt clean
cd web && npm run test:unit 237 pass
cd web && npm run lint 0 errors; 3 pre-existing warnings
node scripts/backlog-sync.mjs --check --offline pass
git diff --check clean

Known limitations and follow-ups

metadata_uri defaults to empty. cdm deploy would normally publish ABI and readme metadata to Bulletin and store that CID here. Doing that properly needs a Bulletin storage authorization, so it is left as a parameter. An empty pointer registers the address correctly but gives cdm install no ABI to fetch — consumers would need the ABI another way until metadata is published.

Not executed. No name is claimed by this PR. Given first-writer-owns is irreversible, that is your call, and the namespace choice should be settled before the first publish.

Registration does not by itself make Dotify composable. Another product can resolve the address, but without published metadata it cannot resolve a typed ABI. The full benefit needs the Bulletin metadata step.


Follow-up: a fresh redeploy was authorised, and it does not unblock cdm deploy

Dotify's on-chain data is test data, so redeploying is acceptable. That removes the objection this task was built around, and it was worth checking properly rather than assuming. cdm deploy still cannot be used — but for a different and harder reason than I expected.

Feasibility is not the blocker. CDM's Solidity path compiles with resolc to PolkaVM. I ran it against Dotify's contracts in a scratch project: all 24 files compile, including the diamond's delegatecall fallback and every one of its 17 inline assembly blocks. Only one informational extcodesize warning from LibDiamond.

Size is the blocker. The Asset Hub initcode limit is 49,152 bytes, and resolc emits roughly 4–10x more bytecode than solc:

Contract Deployed EVM resolc PolkaVM vs 48 KB limit
MusicRegistryPallet 8,855 71,252 over by 45%
SmartRuntime n/a 41,142 under
DiamondCutPallet 4,753 39,408 under
ArtistRuntimeFactory 9,999 38,926 under
ArtistDirectory 1,829 17,325 under
MusicRightsRegistry not deployed 88,955 over by 81%

MusicRegistryPallet holds the catalog, so it is not optional. Clearing the limit would mean splitting it into storage-only and logic contracts — and the practitioner report documenting that workaround also records that diamond-style generic mappings were ineffective at reducing size. That is precisely Dotify's architecture.

What this changes about this PR. Asset Hub's pallet-revive accepts both EVM bytecode via eth-rpc and PolkaVM blobs via resolc. For Dotify the EVM path is not a legacy compromise — it is the only one that currently fits, and every deployed contract sits comfortably inside the limit. So publishLatest registration is the correct mechanism, not a way to dodge a redeploy. The justification for this PR is stronger than when it was opened, and now evidence-backed.

Measured with resolc 0.6.0 in a throwaway project, and eth_getCode against all ten deployed addresses on chain 420420417. Nothing in the repo was migrated.


Follow-up: ABI metadata blobs and their CIDs

This closes the gap flagged above — an empty metadata_uri would have left the registration nominal, since cdm install fails outright without an ABI.

Schema taken from CDM's consumers, not guessed. install.ts validates exactly one thing: the blob must be an object whose abi is a non-empty array. contracts.dot.li additionally renders description, readme, homepage, repository, license, keywords, authors, published_at. Both are satisfied.

Package Blob ABI entries CID
@dotify/artist-directory 4,430 B 11 bafk2bzacebiynqo7tjvxa3xlvf6pphgq3nzatqjbn3yo2fszrlvxibtsr4ce6
@dotify/artist-runtime-factory 7,585 B 18 bafk2bzaceckm27ft3kvt4mjs67hzklylws5fp5d4z4nrvpa34gmzndtqz7lwg

Deterministic on purpose. For a content-addressed artifact this is the difference between being able to verify what is published and having to trust it: the same contracts produce the same bytes and therefore the same CID, so the on-chain pointer can always be checked against the repository. Verified by generating twice and diffing the index.

That is why published_at is omitted by default — a wall-clock field mints a new CID on every run. It is opt-in behind --published-at, at the cost of that property.

CIDs are computed locally with the Bulletin SDK (raw codec 0x55, blake2b-256), so the value published on-chain is derived from the generated bytes. cdm:publish now reads the per-package index rather than taking one hand-pasted URI for all packages — removing the failure mode where a correct-looking CID is attached to the wrong contract:

@dotify/artist-directory
  address:  0xcf1534C6e2B0E43B9436c1e86A076466dC0F2108
  action:   register
  metadata: bafk2bzacebiynqo7tjvxa3xlvf6pphgq3nzatqjbn3yo2fszrlvxibtsr4ce6

@dotify/artist-runtime-factory
  address:  0xBd1a11cFcE8B5Ef7a37E507bC5109895F8F42a72
  action:   register
  metadata: bafk2bzaceckm27ft3kvt4mjs67hzklylws5fp5d4z4nrvpa34gmzndtqz7lwg

The one step I could not do

Uploading the blobs to Bulletin needs a storage authorization — the same finite, expiring quota the frontend publish uses. The runbook now documents the three-step flow, including confirming the returned CID matches cids.json before registering. A mismatch means the bytes changed and must not be registered.

Until the upload happens the CIDs are correct but unresolvable, so do not run --confirm first — that would permanently claim the names against a pointer nothing can fetch.

knzeng-e and others added 24 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>
fetchIpfsCid and fetchAssetRef walked 4-6 gateways serially with no
per-gateway timeout, so one unresponsive gateway cost the listener the
browser's full connection timeout before the next candidate was tried.
Covers, manifests, and v1 audio all read through that path.

Race the candidates instead, the way audioV2Gateway already does for byte
ranges: bound time-to-headers, hedge onto the next gateway after a delay,
cap concurrency at 3, and abort the losers once a winner answers. Every
URL addresses the same immutable CID, so racing them cannot diverge.

The winner's own controller is never aborted, so its body stays readable,
and a caller abort stops the queue where it is rather than walking the
rest of the list.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
min_machines_running = 0 let the API scale to zero, and the first content
key request after an idle period paid the cold start. Measured against the
running deployment: 8.17s to /health cold (uptime 0), 0.11s warm.

That request sits directly in front of first sound, so the saving was
being taken out of the listening experience.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Socket.IO reports a CORS rejection, a stopped server, and a wrong URL
identically, so every one of them surfaced as "Room service unavailable."
That is the vague failure the product invariants rule out, and it hid a
real deployment gap: the running signaling server allows only
muzinga.netlify.app, so opening a room from the Polkadot Product host
origin is refused with a 403 the browser will not explain.

On connect_error, read the server's unauthenticated /health, compare the
page origin against the allowlist it reports, and upgrade the message in
place. The generic reason stands if health cannot be read or if the origin
is allowed, so this only ever widens an error - room access stays decided
by the server.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Deploying with `flyctl deploy -c services/api/fly.toml` from the repository
root fails at `COPY src ./src`: -c selects the config file, but the build
context stays the shell's working directory, and the Dockerfile is written
against services/api. It also uploads a ~1.3 GB context, because Docker reads
.dockerignore from the context root and only the service directories have one.

A cached `npm ci` layer from an earlier correct build hides the cause, so the
error surfaces at the first uncached step rather than the first wrong one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`makeAttempt` detaches its parent-abort listener as soon as the fetch settles,
which is when headers arrive - before the body has streamed. The winner was
therefore returned already disconnected from the caller's signal, so aborting
afterwards no longer stopped the download.

That is a regression against the serial reader this replaced, which passed the
caller's signal straight to `fetch`. It bites where it matters: `useCatalog`
passes a signal to every audio and asset read, so a listener skipping tracks
left the previous audio downloading to completion, unread.

Re-link the winner to the caller's signal before returning it, and abort the
winner on the late-abort path too - it has already been removed from `active`,
so the cleanup block would not otherwise reach it.

Covered by a regression test that failed before this change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Inside the Product host the app is served from polkadot://app.dotify-test01.dot,
not the DotNS web gateway. SIGNAL_ORIGINS already carried it; API_ORIGINS did
not, so a container would have had working rooms and no content keys - catalog
and key delivery both go to the API, so free and protected playback would fail
CORS while the room layer looked healthy.

Deliberately does not add a bare `null`. `polkadot:` is a non-special scheme, so
its origin is opaque and a browser may send `Origin: null` instead of the
literal value. Allowing that would admit every sandboxed iframe and file:// page
on the web to the authenticated upload and content-key routes. If a host request
is still refused, the actual Origin header from the Fly log is the evidence to
act on. Two regression tests pin both halves: the custom-scheme origin is
answered, a null origin is not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Grounded in the Product docs, SDK reference, and Community Foundation repos
rather than assumption. Three findings drive the proposal.

Dotify is closer to the stack than the roadmap assumed. Its Solidity contracts
already run on pallet-revive via Asset Hub's eth-rpc - the exact pallet the
stack specifies - and app delivery, content addressing, and app-scoped identity
are already on-stack. Its DAV2 encryption is not redundant with a Bulletin move
but the precondition for one, since Bulletin gates storing, not reading.

The genuine gaps are worth closing and mostly make the product better: the
personhood precompile returns a per-app unlinkable alias and would finally make
`human-free` real while retiring the dev registrar; CDM registration makes the
catalog composable by other products; CASH is the asset users actually hold.

One gap will not close. Statement Store writes require an Individuality
allowance, official calls are 1:1 and mobile-only, and an SDP exceeds both the
512-byte statement and 1 KiB per-account ceilings. Moving rooms onto the
official messaging layer would convert every listener into an attested person,
which does not degrade the product - it deletes the gesture it exists to
protect. The anonymous guest is therefore treated as a design constraint, not a
legacy compromise.

Proposes a three-ring architecture that shrinks the trusted core instead of
denying it, and names content-key custody as a stated exception with per-artist
custody as the strongest remedy - turning the most centralized component into an
expression of artist sovereignty.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Dotify's `human-free` access mode was gated by an admin registrar that the
runtime initializer sets to the artist. An artist could therefore grant
personhood to their own listeners, which meant the contract could not actually
support the claim the product was making. This replaces that with the
Individuality precompile.

Verified live before writing any of it: the precompile at
0x000000000000000000000000000000000a010000 answers on EVM chain 420420417 -
the chain Dotify is deployed on - returning a 64-byte PersonhoodInfo, where
neighbouring and empty addresses return 0x. The interface is taken from
paseo-network/runtimes, not inferred.

The tiers already lined up: PersonhoodLevel None/DIM1/DIM2 maps by ordinal onto
None/Lite/Full, so no enum or stored track policy changes.

Three properties come out of this. Personhood can no longer be forged by anyone,
including the artist - the setter reverts rather than writing to storage no
access decision reads, because silently accepting it would leave an operator
believing a listener was granted access they do not have. Personhood becomes a
fact about a person rather than per-runtime state, so every runtime reads the
same answer. And the precompile returns a per-context alias, so Dotify can
recognise a distinct person without learning who they are in any other
application - a privacy property the registrar could not offer at all.

Reads use a low-level staticcall on purpose. The precompile declares
HAS_CONTRACT_INFO = false, so its extcodesize can be zero and Solidity's
high-level call would revert on its own extcodesize check. The staticcall also
lets a chain without the precompile resolve to "not live" instead of reverting
every access query, and `live` is surfaced separately so a misconfigured
deployment is not mistaken for an empty user base. Absent precompile denies
every gated track, per the fail-closed invariant.

Tests install mock precompile code at the real address with hardhat_setCode,
which is the only way to exercise both the granted and fail-closed paths on a
chain with no Individuality pallet. Coverage includes tier-below-required,
and personhood held in another application context not unlocking Dotify.

Not deployed. Existing runtimes need a diamond Add cut to gain
musicAccPersonhoodInfo; their access decisions already follow the precompile
without it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`cdm deploy` builds, deploys, publishes metadata, and registers in one pass, so
it cannot be used here: Dotify's contracts are already deployed and already hold
the live catalog. Running it would mint new addresses and orphan every existing
artist runtime.

Reading the registry contract showed the operation that is actually needed is
already exposed. `publishLatest(name, address, metadata_uri)` binds a name to an
arbitrary address, and the contract's own comment states the rule: the caller may
publish only if the name is free or they already own it. So a free name is
claimable directly, no deployment involved. `metadata_uri` is stored verbatim and
never validated.

Adds the `cdm:publish` task for that registration. Read-only by default - it
prints the plan and exact calldata and stops. Registration is first-writer-owns
and the registry exposes no release or transfer entry point, so a claimed name is
permanent; execution therefore requires --confirm and an explicit key, and this
commit claims nothing.

Verified live against the devnet registry at
0x59b0245778917af55224e5f8fb55f7f8d452619f on chain 420420417: all three
@dotify/* names are unclaimed, and both target addresses carry bytecode. Guards
confirmed to fail closed for a wrong registry address and for --confirm without a
key. The task also refuses a target with no bytecode, since publishing a name
that points at nothing is worse than not publishing it.

@dotify/smart-runtime is deliberately not registered: artist runtimes are
per-artist diamonds with no single address, and publishing one artist's runtime
under a shared name would misrepresent the catalog.

Two findings recorded in the architecture doc. CDM's docs independently confirm
the preset distinction this project already encodes - `paseo` is paseo-next
(para 1500), `devnet` is the Paseo testnet Asset Hub (para 1000, chain
420420417). And CDM does support Solidity via a `@custom:cdm` NatSpec tag, which
the architecture page's PolkaVM-only framing hides; Dotify's existing toolchain
is not an obstacle to CDM participation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@netlify

netlify Bot commented Jul 29, 2026

Copy link
Copy Markdown

Deploy Preview for muzinga ready!

Name Link
🔨 Latest commit 185748c
🔍 Latest deploy log https://app.netlify.com/projects/muzinga/deploys/6a6ba663cff8080008b93409
😎 Deploy Preview https://deploy-preview-118--muzinga.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

knzeng-e and others added 3 commits July 30, 2026 02:31
Redeploying was authorised, since Dotify's on-chain data is test data. That
removes the objection this task was built around, so it was worth checking
whether `cdm deploy` becomes viable. It does not, and the reason is a hard chain
limit rather than a preference.

CDM's Solidity path compiles with resolc to PolkaVM. resolc handles Dotify's
contracts fine - all 24 files compile, including the diamond's delegatecall
fallback and every one of its 17 inline assembly blocks, with only an
informational extcodesize warning. Feasibility is not the blocker.

Size is. The Asset Hub initcode limit is 49,152 bytes and resolc emits roughly
4-10x more bytecode than solc. MusicRegistryPallet, which holds the catalog, is
8,855 bytes as deployed EVM bytecode and 71,252 bytes as a PolkaVM blob - 45%
over the limit. MusicRightsRegistry is 81% over. Clearing it would require
splitting the registry into storage and logic contracts, and the practitioner
report documenting that workaround also records that diamond-style generic
mappings were ineffective at reducing size, which is exactly this architecture.

Asset Hub's pallet-revive accepts both EVM bytecode via eth-rpc and PolkaVM
blobs via resolc. For Dotify the EVM path is not a legacy compromise, it is the
only one that currently fits, and every deployed contract sits comfortably
inside the limit. publishLatest registration is therefore the correct mechanism
rather than a way to avoid redeploying.

Measured, not assumed: resolc 0.6.0 compile in a scratch project, and
eth_getCode against all ten deployed addresses on chain 420420417.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The registry stores (name -> address) and (name -> metadata_uri). Registering
only the first leaves the composability claim nominal: another product can
resolve where Dotify's contracts are but not what they expose, and `cdm install`
fails outright because it requires an ABI in the metadata blob.

Shape is taken from CDM's own consumers rather than guessed. `install.ts`
validates exactly one thing - the blob must be an object whose `abi` is a
non-empty array - and contracts.dot.li additionally renders description, readme,
homepage, repository, license, keywords, authors and published_at. Both are
satisfied; the generated blobs pass that validation, with 11 and 18 ABI entries.

Output is deterministic, which matters for a content-addressed artifact: the same
contracts produce the same bytes and therefore the same CID, so what is published
can always be checked against the repository. `published_at` is omitted by
default for exactly that reason and is opt-in behind --published-at, since a
wall-clock field would mint a new CID on every run. Verified by generating twice
and diffing the CID index.

CIDs are computed locally with the Bulletin SDK (raw codec, blake2b-256), so the
value that goes on-chain is derived from the generated bytes. cdm:publish now
reads that index per package instead of taking one hand-pasted URI for all,
which removes the failure mode where a correct-looking CID is attached to the
wrong contract.

Uploading the blobs to Bulletin needs a storage authorization and stays a
separate credentialed step, documented in the runbook. Nothing is published or
registered by this commit.

Also adds TextEncoder to the eslint script globals, which the existing node
globals list was missing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Product SDK cannot do this upload. `CloudStorageClient.create()` resolves its
chain connection through getChainAPI/createChainClient, both of which route
exclusively through the Product host container with no direct-WebSocket
fallback, so uploading from a terminal is impossible that way. The Bulletin
chain itself accepts a plain signed TransactionStorage.store extrinsic over
WebSocket, which is what this script uses - the same approach the existing
deploy-bulletin.cjs already proved.

Read-only by default. It recomputes every CID from the bytes on disk and refuses
to upload when one drifts from cids.json, because a blob edited after generation
would otherwise land under a CID the registry can never match, and the registry
entry is immutable once published. It also reports the account's remaining
Bulletin quota against what the upload needs, and names the two ways to grant
one when it is short.

Separately fixes a real bug in deploy-bulletin.cjs. Its CID builder tagged the
multihash as 0x1e, which is blake3 - an algorithm the Bulletin Chain does not
accept - while the digest was blake2b-256. The correct code is 0xb220, which is
45600 and needs three varint bytes: 0xa0 0xe4 0x02. So the printed CID, the
gateway URL built from it, and any DotNS registration pointing at it resolved
nowhere. Confirmed by decomposing a CID from @parity/bulletin-sdk: the prefix is
01 55 a0 e4 02 20, and the remaining digest equals getContentHash(bytes, 45600).

Note that script cannot currently run at all - blakejs is not installed in
web/node_modules - which is a separate pre-existing issue and is left alone,
since it serves the legacy single-file Bulletin build.

Nothing is uploaded or registered by this commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@knzeng-e knzeng-e self-assigned this Jul 30, 2026
Requiring --private-key was wrong on two counts. The repository already
configures an account for polkadotTestnet from the encrypted PRIVATE_KEY
hardhat var, and it is already set, so the flag was redundant. It was also
worse: a key passed on the command line lands in shell history and process
listings, while the var is encrypted at rest. registryUpgrade.ts already used
hre.viem.getWalletClients() for exactly this reason.

The task now resolves the publisher from the network config and keeps
--private-key only as an explicit override for publishing from a different
account than the deployer.

Also makes the stakes visible. publish_latest records `caller` as the permanent
owner of every name it creates, so the signing account owns @dotify/* from then
on - it is not just paying fees. The dry run now prints the resolved publisher
and says so, since that is the last chance to notice before an irreversible
claim.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@knzeng-e
knzeng-e changed the base branch from main to dev August 3, 2026 13:33
@knzeng-e
knzeng-e marked this pull request as ready for review August 3, 2026 13:34
@knzeng-e
knzeng-e merged commit 43002ba into dev Aug 3, 2026
13 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