Repository navigation
fix(deploy): follow DotNS ABI generations — bump bulletin-deploy, configure the availability probe - #540
Merged
Conversation
…figure the availability probe Deploys on paseo-next-v2 failed for every signer with "Contract execution would revert during startingPrice on POP_RULES". DotNS ships ABI *generations* redeployed behind the SAME contract addresses, rolled out per environment rather than per release. Generation v0.5.8-rc1 removed PopRules.startingPrice(); bulletin-deploy 0.15.0 only knows the older generation and calls it unconditionally. Since the addresses never change, no version diff of environments.json showed anything. - bump bulletin-deploy 0.15.0 -> 0.18.4 (probes the live generation at connect) - availability preflight: resolve the env's DotNS contract map from bulletin-deploy's environments.json and pass it, plus environmentId, to DotNS.connect(). It previously passed only `rpc`, so DotNS fell back to a default map whose POP_RULES has no code here — invisible until 0.16 added the generation probe, because the deploy path (runStorageDeploy) already passed `env` and resolved correctly. - config.test.ts: assert upstream ships that contract map, so a catalog change fails in CI instead of mid-deploy. Verified end to end: storage-and-dotns completes (Bulletin chunks uploaded, DotNS registered) where it previously reverted. Note: `pnpm typecheck` still reports a pre-existing error at playground.ts:509 (Option<Address> owner vs contracts@0.10's HexString), unrelated to this change and tracked separately.
…ess review Follow-up to adae9d5, from code review of that commit. Two regressions introduced there: - `dotnsContractsFor()` was awaited outside the try/catch, so a `loadEnvironments()` failure escaped `checkDomainAvailability` as a throw instead of the documented `{status:"unknown"}` — turning decentralize's warn-and-continue into a hard abort — and skipped `dotns.disconnect()`. Resolved inside the try now, still before `silenceConsole()`. - The new mock's comment claimed connect() assertions that did not exist; the entire fix could be deleted with all 26 tests still green. Added an assertion pinning contracts/tld/network/autoAccountMapping/environmentId. The hand-rolled env lookup is replaced by bulletin-deploy's own `resolveEndpoints(doc, envId)`, which returns the contract map PLUS the fields the first attempt omitted — each of which fails silently rather than loudly: - `tld`: connect() otherwise resolves it on-chain and falls back to "dot" on a revert, so checkOwnership would query `<label>.dot` while we report `<label>.paseo`. - `autoAccountMapping`: omitted, connect() takes the manual-mapping branch, which can submit a `map_account` extrinsic from a read-only path. - `network`: authoritative for isTestnet() instead of spec_name sniffing. Also: - memoize the resolution per env (loadEnvironments is uncached upstream and findAvailableRandomName calls it up to 20x per run) - clear withTimeout's timer (pre-existing): left pending it holds the event loop open for the full 30s after the work completes - guard five contract keys, not just POP_RULES — connect() merges over built-in defaults, so a missing key silently uses a default address with no code on this network. DOTNS_POP_CONTROLLER is dereferenced by the profile probe too; the previous comment claiming otherwise was wrong. - correct three stale docs: the config.test.ts docblock that contradicted its own new test, the min-age justification still describing 0.15.0, and a CLAUDE.md claim citing evidence from the now-unpinned 0.15.0 dist. Not addressed: the outer 30s withTimeout is no longer than one inner connect attempt, so upstream's retries are unreachable and an abandoned race can leak a WS client. Picking a new budget is a UX judgement call, left open.
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
Contributor
|
Dev build ready — try this branch: |
Contributor
E2E Test Pass · ❌ FAILTag:
❌ Failed tests (1)
Sentry traces: view spans for this run |
registry #525 changed `publish` to four arguments (domain, metadata_uri, visibility, modded_from) and changed the selector, so the previous seven-argument call reverts against the deployed contract. The dropped `owner` parameter is the behavioural change worth knowing: the registry now always records `env::caller()`, so dev mode can no longer publish under the signed-in user's identity. `claimedOwnerH160` was renamed `userSessionH160` because it no longer claims anything — it only drives the deploy summary, which now says plainly that a dev-mode app is owned by the dev signer and won't show in MyApps. `is_moddable` and `is_dev_signer` are gone too; moddability lives in the metadata JSON's `repository` field. Also fixes `playground mod` and `playground init`, which failed with `App "<domain>" not found in registry` for every app, including ones that exist. v2 returns `getMetadataUri` as a plain string (empty when absent) rather than an Option, and the `value.isSome` narrowing at both call sites read `undefined` every time. Decoding now lives in one helper that treats an unexpected payload as an error instead of silently reporting "not found" — the shape drift was invisible to tsc because both call sites were untyped.
`PLAYGROUND_PRODUCT_ID` becomes `playground.${getEnvTld()}` — so
`playground.paseo` on paseo-next-v2 — instead of a fixed `playground.dot`.
This is an identity migration: the product id is an input to the account
derivation, so every product account address moves. Anything keyed on the
old id (cached subtrees, the SDK allowance cache under `~/.polkadot-apps/`)
is not found under the new one and is re-granted at the next login. Apps
published under the old address stay owned by it.
It is also required for the CLI to work against a phone on a paseo
environment at all. The mobile host re-validates the product id against its
own TLD in the ProductSubtreeRequest mapper and the throw is swallowed, so a
`.dot` id produced no prompt, no error and no reply — just the full ~180s
deadline. Measured on one session: `playground.paseo` answered in ~3s while
`playground.dot` and `playground.testnet` never did.
playground-app already ships the environment-suffixed form, and the two must
match exactly or the CLI and the app derive different H160s for the same
person, splitting app ownership and XP across two identities.
The pinned test now asserts against getEnvTld() rather than a literal, so
flipping ACTIVE_TESTNET_ENV cannot silently desync the two again. The frozen
address vector was regenerated; the old id still reproduces the old pair,
confirming only the input changed.
deploy, deploy-all, mod and decentralize refused with "Join the competition first" for users whose publish the chain would have accepted. The gate read `getRootAccount` from the identity spine. The registry does not consult the spine — `require_revealed()` delegates to whatever `set_verifier` names, which on this deployment is an open verifier that returns true for every account. The CLI was enforcing a stricter rule than the chain, against a different contract, so the two drifted the moment the deployment wired something else. It now resolves `registry.getVerifier()` and calls `isVerified` on that address, which is correct both today and on a competition deployment that wires the spine, with no flag to keep in sync. The verifier is asked a question rather than identified by address: ADR-0011 names three implementations and an operator picks one per event, so any `verifier === <known address>` check is correct only until the next one. The minimal ABI is attached at runtime so this also works for a verifier that is not in our cdm.json. The option to inject a pre-resolved contract is REMOVED rather than renamed. It existed so a caller holding a registry handle could skip a second meta-registry resolution, and it cannot work any more: the gate reads `getVerifier()` and then queries a different contract, so no handle a caller already has is the right one. `mod` injected its registry anyway — the narrowing is a cast, so tsc stayed silent — and every `mod` run reported "couldn't verify your builder status" on a healthy chain. Removing the parameter makes that unrepresentable instead of merely discouraged; tests mock `getVerifierContract`. `IdentityRegistry` is likewise renamed `PersonhoodVerifier`, since the old name is what made passing a registry look right. `getReadOnlyIdentityContract` is kept but documented as deliberately unused — the spine is real and a reveal flow will need it, but it must not come back as the gate. Adds coverage for `getVerifierContract`, including that it follows whatever `getVerifier()` returns rather than a hardcoded address, so a future "optimisation" to a constant fails the build.
This was referenced Sep 22, 2026
feat: migrate to v2 playground contracts, env-scoped product id, and gate on the wired verifier
#545
Merged
CI's Typecheck failed on this PR: `Argument of type 'Contract<ContractDef>' is not assignable to parameter of type 'MetadataUriReader'`. The registry handle only carries its per-method types when `.cdm/` has been generated by `cdm install`. That directory is gitignored and CI never creates it, so there the handle is a bare `Contract<ContractDef>` with no `getMetadataUri` at all — no structural interface can match it. Locally, with `.cdm/` present, the same code type-checks cleanly, which is why this could not be caught before pushing. Cast at the single call site. `getAppMetadataUri` validates the response shape at runtime and throws on anything unexpected, so the cast costs no real safety. Verified under both conditions by temporarily hiding `.cdm/` and re-running `tsc`: zero errors with and without it.
`.cdm/` was gitignored wholesale and CI never generated it — the `cdm` CLI is a global tool that needs chain access, not a repo dependency. So the `Typecheck` job was checking everything EXCEPT the contract call sites: with no module augmentation, every registry handle is a bare `Contract<ContractDef>` and `.query()`/`.tx()` calls go unchecked. That is not hypothetical. It hid the 7-argument `publish` call against a 4-argument v2 contract, and a `getMetadataUri` shape bug that broke `playground mod` and `playground init` for every app. It also produces the inverse trap, which this PR hit: code that compiles locally fails in CI, because a structural interface cannot match the un-augmented type. Commit only the two `.d.ts` module augmentations. The rest of `.cdm/` stays ignored: `contracts/**/info.json` snapshots per-version contract addresses we never read (they resolve live from the CDM meta-registry), so committing them would add a second, stale source of truth. Verified by hiding `.cdm/contracts/` — leaving exactly what CI will have — and re-running `tsc`: zero errors. The license check now skips `.cdm/`. Those files are generated and marked "do not edit", so a header would be stripped by the next `cdm i` and turn the check red on a routine regeneration.
Moves to @parity/product-sdk-terminal@^0.10.0 (host-papp 0.10.0), which carries product-sdk #364 and #366 together. Pairing has been impossible since mid-August. host-papp 0.9.0 moved the SSO envelope from P-256/AES-GCM to X25519/ChaCha20-Poly1305, shrinking the device key from 65 to 32 bytes; the handshake codec is fixed-width, so a 65-byte sender is undecodable rather than degraded — iOS said "Something went wrong", Android "invalid QR code". Every terminal up to 0.8.2 pinned host-papp ^0.8.9, whose 0.x ceiling is the last release of the old wire, so no bump below 0.9 could have helped. Do NOT take 0.9.x on the way past: that is #364 without #366, where pairing succeeds and then checkMapping fails permanently, because the phone mints PGAS to the RFC-0022 address while an unmigrated CLI checks the old one — reported in the UI as transient, so it loops forever. RFC-0022 puts the product account at //product//{productId}/{index} with the two //product//{productId} junctions HARD. A public key cannot cross a hard junction, so the parent is now a subtree key the wallet provides, and the derivation is async. `deriveSessionAddresses` becomes async with it. Two things the pre-merge notes got wrong, both caught here: - `ProductAccountRef.derivationIndex` stayed a plain number. The tagged DerivationIndex lives inside @parity/product-sdk-keys, whose `deriveProductAccountPublicKey(subtree, {tag,value})` is a 2-arg form, so the root moves to keys ^0.4.0. On 0.3.x that call is a tsc error; bypass tsc and it derives from a stringified object instead. Worth noting because a vitest-only run does not typecheck — which is how it reached a test run at all. - `publicKey` on the ref is optional; we omit it deliberately so the signer and the address we display resolve through the same cached subtree and cannot desync. Tests pin the derivation at the primitive against host-rust-core's own cross-host vector (wasm_crypto_vectors.rs), not against our output — a fixture regenerated from the code it checks is worthless. They also mock the SDK's subtree fetch: unmocked it hangs waiting for a phone and writes a bogus key into the developer's real ~/.polkadot-apps cache, breaking their live session. `productAccount.test.ts` is deleted: the product id no longer enters that primitive, so it could no longer test the wiring it claimed to, and its assertions are subsumed by the exact vector test. Verified against a real paired session: the built binary derives 0x8755a050e0b47300a7454942dac4f5dafd8222e5 — the address playground-app prints and the owner of the apps deployed today. Users pair again once (playground logout + playground login); anything already published stays owned by the old address.
…ape, expiry path
Four defects found by running the migrated binary end to end against a real
phone. All of them passed the unit suite and a clean build; the first two were
hidden because `pnpm exec tsc` was exiting inside pnpm's deps-status check
(the release-age policy) before tsc ran, so a grep for "error TS" reported a
clean typecheck that never happened. Run tsc directly.
- identityGate.ts read `.productH160` off a Promise, so every gated command
(deploy, deploy-all, mod, decentralize) refused with "Couldn't verify your
builder status" on a healthy chain.
- DripScreen.tsx had the same missed await.
- The SmartContractAllowance destination was a bare `0` instead of the
tagged `{tag:"Index",value:0}`, in two places. The grant still succeeds on
the phone, so it looks fine — but the SDK sub-keys its allowance cache by
destination, and an untagged value takes the `Raw` branch and hexes
`undefined`, so every later read died with "undefined is not an object
(evaluating 'bytes.length')" and login reported "allowances ✕". Latent
before 0.10.0, which started reading that key.
- `wrapSignerWithSssFastFail` only detected the pre-0.10 expiry shape (a
`console.error` line plus a hang). 0.10.0 REJECTS with
`AllowanceExpiredError` instead, so the wrapper stopped producing the one
message that carries the remedy — re-pairing, because a renewal request
would travel over the expired channel. It now handles both, keeps the
original as `cause`, and matches by name as well as `instanceof` (a pnpm
tree can hold two copies of the SDK, where `instanceof` silently fails).
Deploy also stops presenting a retry as progress. The phone-approval counter
counted signature REQUESTS, so a timed-out request re-sent as "step 2" read as
forward progress and contradicted the plan above it, which counts operations.
Observed live: "step 1: Link content" then "step 2: Link content" while the
plan said step 2 was the registry publish, and four prompts arrived for a
two-approval deploy.
Verified against a real session: pairing, login, allowances, mapping, the
identity gate, phone signing, deploy, and re-publish of an app owned by the
product account (which also confirms v2 republish authorization).
…ing fix
bulletin-deploy 0.18.4 -> 0.20.0. Verified against paseo-next-v2 with a
dev-signer deploy of a NEW domain (bd-0200-devtest.paseo): build, chunk
upload, DotNS register, playground publish, owner recorded as the dev signer,
and the deployed URL returns HTTP 200. The HTTP check is the part that
matters — a deploy reporting success has never been sufficient evidence that
the CAR was parseable.
0.20.0 fixes a crash we actually hit: `JSON.parse` on incoming RPC data was
unguarded and threw inside the socket handler where nothing upstream could
catch it, killing the process mid-deploy. That is the
`SyntaxError: JSON Parse error: Unexpected EOF` at `_onMessage` seen during a
template deploy. 0.19.x additionally drops the storage-deposit floor for
registering a name from a hardcoded 200 PAS to 5 PAS, stops registers
reverting when the chain moves between dry-run and inclusion, and uploads
duplicate bundle files once instead of twice.
Checked per the invariant before pinning: `deploy()`'s signature is unchanged,
every `DeployOptions` field we pass still exists, the phase banners and `[N/M]`
chunk lines the progress parser matches are intact, and the `patch-package`
postinstall is still shipped (so the `allowBuilds` denial stays).
CLAUDE.md gains two notes from this session:
- `pnpm <script>` can exit in its deps-status check BEFORE running the tool,
which looks like a pass if you grep the output for "error TS". That is how
four type errors reached a live phone test. Check the exit code or run the
binary directly.
- Mapping is not required to CALL a contract, and `checkMapping` returning
false is normal for a plain Substrate signer. Every AccountId32 has a
deterministic H160; `Revive.OriginalAccount` is only the reverse map.
Contract fees can be paid in PAS, so neither PGAS nor mapping should be
blamed for a rejected extrinsic without evidence — both were, wrongly.
The sentry scripts looked up keychain item `sentry-api-token`, which does not
exist; the real one is `sentry-api-token-playground`. All three would have
failed for anyone following the documented backup-before-change workflow.
ReinhardHatko
marked this pull request as ready for review
September 28, 2026 11:17
feat(auth): fix QR pairing and sign as the RFC-0022 product account (product-sdk-terminal 0.10.0)
feat: migrate to v2 playground contracts, env-scoped product id, and gate on the wired verifier
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Deploys on paseo-next-v2 failed for every signer with:
Phone mode and
--signer devfailed identically, with identical gas figures — which looked like a chain/contract outage but was not.Cause: DotNS ships ABI generations
DotNS is redeployed in ABI generations behind the same contract addresses, and they roll out per environment, not per release. So a version diff of
environments.jsonshows nothing while the on-chain ABI has moved underneath you.poprules-startingPricePopRules.startingPrice()v0.5.8-rc1maxPrice,pricingVersion)PopRules.price(label)—startingPrice()removed, now revertsv0.6.0DotnsPopController.isPopIssuedOur pinned
bulletin-deploy@0.15.0knows only the first generation and callsstartingPrice()unconditionally.0.16+probes the live generation at connect time.Changes
Bump
bulletin-deploy0.15.0 → 0.18.4. Verified before bumping: index exports additive only, everyDeployOptionsfield we pass still present, and the banner stringsprogress.tsparses unchanged (confirmed live —chunk 1/2,chunk 2/2still render).Fix the availability preflight. It constructed
DotNSand connected with onlyrpc. With no contract map, DotNS merges nothing over its built-in defaults — whosePOP_RULEShas no code on this network — so0.16+'s probe failed with "No contract deployed at this address". This was invisible before0.16because that version didn't probe, and the deploy path (runStorageDeploy) already passedenvand resolved correctly. Two call sites, two levels of configuration.Now resolved through bulletin-deploy's own
resolveEndpoints(doc, envId), which also supplies fields the first attempt missed — each failing silently rather than loudly:tld— otherwise resolved on-chain with a"dot"fallback on revert, socheckOwnershipwould query<label>.dotwhile we report<label>.paseo.autoAccountMapping— omitted,connect()takes the manual branch, which can submit amap_accountextrinsic from a read-only path.network— authoritative forisTestnet()instead of spec_name sniffing.Widen the divergence guard to five contract keys.
connect()merges over defaults, so a key missing upstream silently uses a dead default rather than erroring.DOTNS_POP_CONTROLLERis dereferenced by the profile probe too.Clear
withTimeout's timer (pre-existing leak): left pending it holds the event loop open for 30 s after the work completes — the "hangs after the work is visibly finished" symptom in CLAUDE.md.findAvailableRandomNamecalls it up to 20× per run.Memoize the env resolution (
loadEnvironmentsis uncached upstream).CLAUDE.md: document the generations model and the two diagnosis traps — comparing contract addresses proves nothing, and two different signers failing identically means "our client is stale", not "the chain is down".
Verified live
A deploy that previously reverted at
startingPricenow completes:Bulletin upload and DotNS registration both succeed. The subsequent
playgroundstep fails separately because@w3s/playground-registryis not currently published to the CDM meta-registry on this env — unrelated to this PR, and being handled.Note on a local-only typecheck error
While developing this I hit
src/utils/deploy/playground.ts:509—Option<Address>owner vs
contracts@0.10'sHexString— and initially expected it to fail CI.It does not:
Typecheckpasses in CI — but not for the reason first given here.An earlier version of this note blamed a Node version difference (local 24.7.0 vs
CI 22). That was wrong.
The actual cause:
.cdm/is gitignored (.gitignore:4) and CI never generates it,while
tsconfig.jsonincludes./.cdm/**/*. That directory holdscdm install'sgenerated
contracts.d.ts, which augments@parity/product-sdk-contractswith thereal contract signatures. Locally the augmentation is present, so tsc checks contract
calls against the actual ABI; in CI the include matches nothing and those call sites
go effectively unchecked, so the error cannot surface.
So CI's
Typecheckjob does not cover contract call sites at all — a contract ABIchange can land green and break every deploy at runtime.
Fixed in #545, though not the way suggested here: CI cannot run
cdm install(thecdmCLI is a global tool that needs chain access, not a repo dependency), so the twogenerated
.d.tsmodule augmentations are committed instead and the rest of.cdm/stays ignored. That gap has since let two real defects through — a 7-argument
publishagainst a 4-argument contract, and a
getMetadataUrishape change that brokeplayground mod/initfor every app.The error itself is unrelated to this PR either way — that file is untouched, and the
lockfile diff contains only the
bulletin-deployline.Testing
pnpm test1022 passed ·format:check✓ ·lint:license✓ ·typecheck— only the pre-existing error above.A regression test now pins the
connect()arguments. Without it the entire fix could be deleted with every other test still green.