Skip to content

fix: hard coded TLD - #261

Merged
sphamjoli merged 5 commits into
mainfrom
spha/dynamic-tld
Aug 19, 2026
Merged

fix: hard coded TLD#261
sphamjoli merged 5 commits into
mainfrom
spha/dynamic-tld

Conversation

@sphamjoli

@sphamjoli sphamjoli commented Aug 18, 2026

Copy link
Copy Markdown
Member

Description

The SDK derived every domain node and token id from a hardcoded namehash('dot'), but the TLD is a per-deployment value fixed at DotnsProtocolRegistry initialisation (for example, paseo on the Paseo testnet, whose tld() returns .paseo). On such deployments, every ownerOf/available/subnode read queried the wrong token id, so the availability pre-check disagreed with the on-chain register(), surfacing as NameNotAvailable(getsome) after a passing check.

This PR resolves the TLD from chain across the whole CLI, so no code path assumes .dot, and adds a pre-commit gate against a related code-quality issue (dangling inline types).

Core resolution

  • add deriveDomainNode/deriveDomainTokenId pure helpers keyed on tldNode
  • add core/naming: resolve tldNode/tld once per deployment from the controller's protocolRegistry (cached), and derive ids, nodes and display names from it, replacing the hardcoded DOT_NODE (now removed). resolveTldInfo strips the registry's leading-dot suffix (.paseo becomes paseo)
  • make ensureDomainNotRegistered call the controller's available(label), the exact predicate register() enforces, instead of inferring from a swallowed ownerOf revert

Full TLD sweep

  • subname registration now roots the parent node under the on-chain TLD via domainNode(ctx, parent) instead of namehash(\${parent}.dot`)`
  • performDomainLookup derives the node and display name from the chain TLD
  • normaliseLabel(name, tld) strips the deployment's TLD suffix, which is what distinguishes a second-level name (alice.paseo) from a subdomain (sub.alice); a new normaliseName(ctx, name) resolves the TLD and applies it
  • every display string and result name uses formatDomainName(ctx, label) rather than a hardcoded .dot; cross-environment cache listings show the bare label
  • static help text and doc comments made TLD-neutral

Ergonomics

  • print using dotns cli vX.Y.Z on every operation
  • make the argv-secret warning loud and yellow
  • bump CLI to 0.8.3

Tooling

  • add scripts/checkDanglingTypes.mjs: an AST gate that blocks new inline object shapes in function signatures, generics and type assertions. Wired into the pre-commit hook in diff-scoped (--staged) mode so it only fails on newly introduced ones, and exposed as bun run check:types / check:types:staged

Tests

  • unit tests extended in the validation, contractInteractions and registrationManifest suites (no new test files)
  • integration tests resolve the expected TLD for the target environment (DOTNS_ENV) via a cached helper, so assertions never check against the wrong TLD

Type

  • Bug fix
  • Feature
  • Breaking change
  • Documentation
  • Chore (pre-commit dangling-type gate)

Package

  • @parity/dotns-cli
  • Root/monorepo (root scripts, .githooks/pre-commit, scripts/checkDanglingTypes.mjs)
  • Documentation

Related Issues

Fixes

#225

Registrations and lookups on non-.dot deployments (paseo-v2, previewnet) behaving incorrectly because the SDK assumed a .dot TLD: registration failing with NameNotAvailable after a passing availability pre-check, subname parent nodes computed under the wrong TLD, and names displayed with the wrong suffix.

Checklist

Code

  • Follows project style
  • bun run lint passes
  • bun run format passes
  • bun run typecheck passes

Documentation

  • README updated if needed (no change needed)
  • Types updated if needed (no change needed)

Breaking Changes

  • No breaking changes
  • Breaking changes documented below

computeDomainTokenId moved from utils/contractInteractions to core/naming and is now asynchronous and context-aware, and normaliseLabel gained an optional tld argument (defaulting to dot). Both are internal helpers, not part of the published package surface, so consumers are unaffected.

Breaking changes:

None.

Testing

How to test:

  1. cd packages/cli && bun run typecheck && bun run lint && bun test tests/unit/ (298 unit tests pass).
  2. Confirm the resolution fix against a non-.dot deployment: bun run src/cli/index.ts lookup owner-of getsome --env paseo-v2 reports Registered: true under the real TLD, whereas before the change it wrongly reported not registered.
  3. Confirm display uses the real TLD: bun run src/cli/index.ts lookup name getsome --env paseo-v2 shows domain: getsome.paseo (single dot), not getsome.dot.
  4. Register a fresh name end to end: DOTNS_MNEMONIC=... bun run src/cli/index.ts register domain --name <freeLabel> --env paseo-v2 completes through commit-reveal instead of reverting NameNotAvailable.
  5. Confirm every operation prints using dotns cli v0.8.3, and that --mnemonic prints a loud yellow security warning.
  6. Verify the pre-commit gate: bun run check:types lists existing debt; bun run check:types:staged blocks a commit that stages a new inline object shape in a signature, generic or assertion, and passes otherwise.

Notes

  • Verified end to end on paseo-v2: siphamandla registered successfully (tx 0xc5e78acb1d87399e6ff03e5b7df6971a7f6c3cd3d05c6a833ea14ee530b8db64, 0 PAS), and getsome was confirmed already registered under .paseo, which was the original failure.
  • The TLD is read once per deployment and cached, since DotnsProtocolRegistry.tldNode() is immutable after initialisation.
  • The dangling-type gate is diff-scoped: 71 pre-existing findings are grandfathered so the gate blocks only newly introduced inline types rather than forcing a repo-wide retrofit.

…ed .dot

The SDK derived every domain node and token id from a hardcoded
namehash('dot'), but the TLD is a per-deployment value fixed at
DotnsProtocolRegistry initialisation (for example 'paseo' on the Paseo
testnet). On such deployments every ownerOf/available read queried the
wrong token id, so the availability pre-check disagreed with the on-chain
register(), surfacing as NameNotAvailable(getsome) after a passing check.

- add deriveDomainNode/deriveDomainTokenId pure helpers keyed on tldNode
- add core/naming: resolve tldNode/tld once per deployment from the
  controller's protocolRegistry (cached) and derive ids and display names
  from it, replacing the hardcoded DOT_NODE
- make ensureDomainNotRegistered call the controller's available(label),
  the exact predicate register() enforces, instead of inferring from a
  swallowed ownerOf revert
- print "using dotns cli vX.Y.Z" on every operation
- make the argv-secret warning loud and yellow
- bump CLI to 0.8.3

Tests extended in the contractInteractions and registrationManifest suites.
@github-actions

github-actions Bot commented Aug 18, 2026

Copy link
Copy Markdown

CI Summary

Check Result
Lint Passed
Format Passed
Typecheck Passed
Build Passed
Release Passed
Deploy Example Failed
Benchmark UI Failed
PR Title Passed
Labels Passed
Test Passed - 300 passed, 0 failed

Release - Passed

Test this PR

Download artifact (GitHub CLI required):

gh run download 32233936201 -n cli-release-0.0.0-pr.261 -R paritytech/dotns-sdk

Install globally:

npm install -g ./parity-dotns-cli-0.0.0-pr.261.tgz

Verify:

dotns --help
Deploy Example — Failed

Failed at: Deploy — Deploy workflow failed — see run logs for upload/register/contenthash details

Stage Status
✓ Site validation Site validated
✗ Deploy Deploy workflow failed — see run logs for upload/register/contenthash details

View run

Benchmark UI - Failed (0s)
Property Value
Duration 0s
Domain n/a
CID n/a
Workflow view
Labels

pkg: cli, type: test, dependencies

Test - Passed

300 passed, 0 failed across 300 tests.

View run

$ bun test tests/unit/
bun test v1.2.6 (8ebd5d53)

::group::tests/unit/delegate/delegateHelp.test.ts:
(pass) delegate --help lists set, revoke, status, records and auth options [15.00ms]
(pass) delegate records --help shows operator argument and --revoke [3.00ms]
(pass) delegate records-status --help shows operator argument and --json [3.00ms]
(pass) delegate set --help shows name and delegate arguments [2.00ms]
(pass) delegate revoke --help shows name argument [2.00ms]
(pass) delegate status --help shows name argument and --json [2.00ms]

::endgroup::

::group::tests/unit/pop/setPopHelp.test.ts:
(pass) root help lists pop command [2.00ms]
(pass) pop help shows commands and description [2.00ms]
(pass) pop help shows auth options [2.00ms]
(pass) pop info help shows description [2.00ms]
(pass) pop status help shows description [2.00ms]
(pass) pop info help shows auth options [2.00ms]
(pass) pop info parses auth options at pop level [3.00ms]
(pass) pop info parses auth options at info level [3.00ms]
(pass) pop help command shows pop help [2.00ms]
(pass) pop help status shows info help [3.00ms]
(pass) pop help info shows info help [2.00ms]

::endgroup::

::group::tests/unit/pop/popJson.test.ts:
(pass) pop info --help shows --json option [3.00ms]
(pass) pop status --help shows --json option [2.00ms]

::endgroup::

::group::tests/unit/escrow/escrowHelp.test.ts:
(pass) root help lists escrow command [2.00ms]
(pass) escrow help shows the subcommand description and subcommands [2.00ms]
(pass) escrow balance help describes the claimable pull-payment balance [1.00ms]
(pass) escrow positions help describes the list and total [2.00ms]
(pass) escrow status help describes the read-only position lookup [1.00ms]
(pass) escrow release help describes the approve-and-release sequence [1.00ms]
(pass) escrow withdraw help describes the post-cooldown step [2.00ms]
(pass) escrow claim-withdrawal help describes the overpayment ledger [1.00ms]
(pass) escrow refunds help lists list/claim/claim-batch [2.00ms]
(pass) escrow refunds list help exposes pagination options [1.00ms]
(pass) escrow refunds claim help requires an entry id positional [2.00ms]
(pass) escrow refunds claim-batch help accepts variadic ids [2.00ms]

::endgroup::

::group::tests/unit/escrow/escrowFormatting.test.ts:
(pass) formatRefundEntryLine > marks entries past their cooldown as claimable [1.00ms]
(pass) formatRefundEntryLine > marks entries inside their cooldown window with remaining seconds
(pass) formatRefundEntryLine > renders the amount as a PAS decimal string
(pass) formatRefundEntryLine > truncates large tokenIds for terminal display
(pass) totalEscrowAmount > is zero for no positions
(pass) totalEscrowAmount > sums position amounts
(pass) isRefundableDeposit > is true only when the position holds an amount
(pass) cooldownRemainingSeconds > returns the seconds left, clamped to zero once elapsed
(pass) formatCooldown > formats seconds and minutes
(pass) formatPositionStatus > held when not released
(pass) formatPositionStatus > embeds the cooldown countdown while released and waiting
(pass) formatPositionStatus > claimable once the cooldown elapses
(pass) formatPositionStatus > claimed after withdrawal
(pass) formatPositionsTable > returns no lines for an empty set
(pass) formatPositionsTable > renders a header plus one aligned row per position with the cooldown
(pass) pendingRefunds multi-output decode > decodes to a [ids, entries] tuple [4.00ms]
(pass) pendingRefunds multi-output decode > decodes an empty ledger to a pair of empty arrays

::endgroup::

::group::tests/unit/lookup/lookupHelp.test.ts:
(pass) lookup --help lists subcommands and auth options [2.00ms]
(pass) lookup name --help shows label argument and options [1.00ms]
(pass) lookup owner-of --help shows label argument and options [2.00ms]
(pass) lookup transfer --help shows label argument and destination option [2.00ms]
(pass) lookup transfer parses destination at transfer level [2.00ms]
(pass) lookup transfer parses auth options at lookup level [1.00ms]

::endgroup::

::group::tests/unit/store/storeHelp.test.ts:
(pass) store --help lists subcommands [3.00ms]
(pass) store claim --help shows description and auth options [1.00ms]
(pass) store info --help shows auth options [1.00ms]
(pass) store list --help shows options [2.00ms]
(pass) store get --help shows key argument [2.00ms]
(pass) store set --help shows key and value arguments with auth [2.00ms]
(pass) store delete --help shows key argument with auth [3.00ms]

::endgroup::

::group::tests/unit/auth/authHelp.test.ts:
(pass) root help lists auth command [2.00ms]
(pass) auth help shows options and subcommands [2.00ms]
(pass) auth set help shows all options [2.00ms]
(pass) auth list help shows options [2.00ms]
(pass) auth use help shows options [2.00ms]
(pass) auth remove help shows options [2.00ms]
(pass) auth clear help shows options [2.00ms]
(pass) auth parses keystore-path option [2.00ms]
(pass) auth parses password option [2.00ms]
(pass) auth set parses account option [2.00ms]
(pass) auth set parses mnemonic option [1.00ms]
(pass) auth set parses key-uri option [2.00ms]
(pass) auth help command shows help [1.00ms]
(pass) auth help set shows set command help [2.00ms]
(pass) auth help list shows list command help [1.00ms]
(pass) auth help use shows use command help [2.00ms]
(pass) auth help remove shows remove command help [2.00ms]
(pass) auth help clear shows clear command help [1.00ms]

::endgroup::

::group::tests/unit/auth/resolveAuthSourceReadOnly.test.ts:
(pass) resolveAuthSourceReadOnly honours environment variables > $label [1.00ms]
(pass) resolveAuthSourceReadOnly honours environment variables > $label
(pass) resolveAuthSourceReadOnly honours environment variables > $label
(pass) resolveAuthSourceReadOnly honours environment variables > $label
(pass) resolveAuthSourceReadOnly honours environment variables > $label

::endgroup::

::group::tests/unit/auth/authRevert.test.ts:
(pass) auth set rejects account name with forward slash [3.00ms]
(pass) auth set rejects account name with backslash [1.00ms]
(pass) auth set rejects account name that is just a dot [2.00ms]
(pass) auth set rejects account name that is double dots [1.00ms]
(pass) auth set rejects account name starting with dot [2.00ms]
(pass) auth set rejects account name ending with dot [1.00ms]
(pass) auth set rejects account name with special characters [11.00ms]
(pass) auth set rejects account name that is too long [2.00ms]
(pass) auth use rejects non-existent account [79.00ms]
(pass) auth remove rejects non-existent account [64.00ms]

::endgroup::

::group::tests/unit/auth/auth.test.ts:
(pass) auth set creates keystore and stores multiple accounts [270.00ms]
(pass) auth set accepts account names with special characters [376.00ms]
(pass) auth list reports missing keystore [5.00ms]
(pass) auth list shows all accounts and auth types [244.00ms]
(pass) auth use switches default account [359.00ms]
(pass) auth remove last account clears default [74.00ms]
(pass) auth remove preserves remaining accounts and reassigns default [193.00ms]
(pass) auth clear deletes all accounts [62.00ms]
(pass) resolveAuthSource prefers selected keystore account over ambient env mnemonic [191.00ms]
(pass) resolveAuthSource resolves from env mnemonic when only the default keystore path is given
(pass) resolveAuthSource prefers selected keystore account over ambient env key URI [118.00ms]
(pass) auth set rejects weak password supplied by flag for new keystore [4.00ms]
(pass) auth set rejects weak password supplied by env for new keystore [2.00ms]

::endgroup::

::group::tests/unit/content/contentJson.test.ts:
(pass) content view --help shows --json option [2.00ms]
(pass) content set --help shows --json option [2.00ms]
(pass) content set --json emits JSON error when both mnemonic and key-uri provided [2.00ms]

::endgroup::

::group::tests/unit/content/contentHelp.test.ts:
result:  {
  exitCode: 0,
  standardOutput: "Usage: dotns content [options] [command]\n\nManage domain content hashes\n\nOptions:\n  --env <environment>            DotNS environment: paseo-v2 (env: DOTNS_ENV)\n  --network <environment>        Alias for --env\n  --rpc <wsUrl>                  WebSocket RPC endpoint (env: DOTNS_RPC)\n  --keystore-path <path>         Keystore path (env: DOTNS_KEYSTORE_PATH)\n  --account <name>               Keystore account name (default: keystore\n                                 default)\n  --password <pw>                Keystore password (env:\n                                 DOTNS_KEYSTORE_PASSWORD)\n  -m, --mnemonic <phrase>        BIP39 mnemonic phrase (env: DOTNS_MNEMONIC)\n  -k, --key-uri <uri>            Substrate key URI (env: DOTNS_KEY_URI)\n  --signer <kind>                Signer: keystore (default) or qr [experimental]\n                                 (env: DOTNS_SIGNER) (default: \"keystore\")\n  --qr-app-id <id>               App id for QR pairing (env: DOTNS_QR_APP_ID)\n  --qr-people-rpc <stageOrUrls>  QR pairing relay: paseo|preview|stable or wss\n                                 URLs (env: DOTNS_QR_PEOPLE_RPC)\n  --qr-fresh                     Force a fresh QR pairing, ignoring any cached\n                                 session\n  -h, --help                     display help for command\n\nCommands:\n  view [options] <name>          View domain content hash\n  set [options] <name> <cid>     Set domain content hash (IPFS CID)\n  help [command]                 display help for command\n",
  standardError: "",
  combinedOutput: "Usage: dotns content [options] [command]\n\nManage domain content hashes\n\nOptions:\n  --env <environment>            DotNS environment: paseo-v2 (env: DOTNS_ENV)\n  --network <environment>        Alias for --env\n  --rpc <wsUrl>                  WebSocket RPC endpoint (env: DOTNS_RPC)\n  --keystore-path <path>         Keystore path (env: DOTNS_KEYSTORE_PATH)\n  --account <name>               Keystore account name (default: keystore\n                                 default)\n  --password <pw>                Keystore password (env:\n                                 DOTNS_KEYSTORE_PASSWORD)\n  -m, --mnemonic <phrase>        BIP39 mnemonic phrase (env: DOTNS_MNEMONIC)\n  -k, --key-uri <uri>            Substrate key URI (env: DOTNS_KEY_URI)\n  --signer <kind>                Signer: keystore (default) or qr [experimental]\n                                 (env: DOTNS_SIGNER) (default: \"keystore\")\n  --qr-app-id <id>               App id for QR pairing (env: DOTNS_QR_APP_ID)\n  --qr-people-rpc <stageOrUrls>  QR pairing relay: paseo|preview|stable or wss\n                                 URLs (env: DOTNS_QR_PEOPLE_RPC)\n  --qr-fresh                     Force a fresh QR pairing, ignoring any cached\n                                 session\n  -h, --help                     display help for command\n\nCommands:\n  view [options] <name>          View domain content hash\n  set [options] <name> <cid>     Set domain content hash (IPFS CID)\n  help [command]                 display help for command\n",
}
(pass) content --help lists subcommands and auth options [3.00ms]
(pass) content view --help shows name argument and options [2.00ms]
(pass) content set --help shows name and cid arguments [2.00ms]

::endgroup::

::group::tests/unit/primary/primaryHelp.test.ts:
(pass) primary --help lists set, status and auth options [2.00ms]
(pass) primary set --help shows name argument and --json [2.00ms]
(pass) primary status --help shows optional address argument and --json [1.00ms]

::endgroup::

::group::tests/unit/bulletin/bulletinAuthorizer.test.ts:
(pass) bulletin authorizer default > defaults to //Eve, the seeded AllowedAuthorizers account
(pass) bulletin authorizer default > is distinct from the sudo key, which stays //Alice
(pass) warnIfDevKeyOnTestnet > warns when the default authorizer is used against previewnet
(pass) warnIfDevKeyOnTestnet > stays silent on paseo-v2 even with the default authorizer
(pass) warnIfDevKeyOnTestnet > stays silent when an explicit signer overrides the default
(pass) getPreviewUrl > returns an https preview link on the environment that has a web app
(pass) getPreviewUrl > returns null (never throws) on an environment with no web app

::endgroup::

::group::tests/unit/bulletin/uploadManifest.test.ts:
(pass) upload manifest resume behavior > returns stale manifest when fingerprint does not match [2.00ms]
(pass) upload manifest resume behavior > deduplicates completed blocks by index [1.00ms]

::endgroup::

::group::tests/unit/bulletin/uploadProfiling.test.ts:
(pass) upload profiler > writes schema-complete profile report with peak aggregation [22.00ms]
(pass) upload profiler > default profile path is deterministic for a given fingerprint

::endgroup::

::group::tests/unit/bulletin/bulletinCliHelp.test.ts:
(pass) root help lists bulletin command [2.00ms]
(pass) bulletin help shows commands and description [1.00ms]
(pass) bulletin upload help shows all options [2.00ms]
(pass) bulletin upload help shows default values [2.00ms]
(pass) bulletin authorize help shows all options [1.00ms]
(pass) bulletin authorize help shows default values [2.00ms]
(pass) bulletin history help shows options [1.00ms]
(pass) bulletin history:remove help shows usage [2.00ms]
(pass) bulletin history:clear help shows description [1.00ms]
(pass) bulletin help command shows bulletin help [2.00ms]
(pass) bulletin help upload shows upload help [2.00ms]
(pass) bulletin help authorize shows authorize help [1.00ms]
(pass) bulletin status help shows all options [2.00ms]
(pass) bulletin help status shows status help [2.00ms]
(pass) bulletin list alias works [1.00ms]
(pass) bulletin verify help shows usage [2.00ms]
(pass) bulletin help verify shows verify help [1.00ms]

::endgroup::

::group::tests/unit/utils/validation.test.ts:
(pass) normaliseLabel > lowercases, trims, and strips a single trailing .dot [1.00ms]
(pass) normaliseLabel > strips the given TLD suffix rather than assuming .dot
(pass) isSecondLevelDotName > distinguishes a second-level name from a subdomain under the given TLD
(pass) isCanonicalLabel > accepts lowercase letters, digits and internal hyphens
(pass) isCanonicalLabel > rejects dots, uppercase, spaces and other characters
(pass) isCanonicalLabel > rejects leading or trailing hyphens, empty and overlong labels
(pass) isSecondLevelDotName > accepts a single label, with or without the .dot suffix
(pass) isSecondLevelDotName > rejects subdomains
(pass) validateCanonicalLabel > throws for a name containing a dot, naming the role [1.00ms]
(pass) validateCanonicalLabel > does not throw for a canonical label
(pass) countTrailingDigits > returns 0 for a label with no trailing digits
(pass) countTrailingDigits > returns the digit run length for a label with trailing digits
(pass) countTrailingDigits > does not count interior digits
(pass) stripTrailingDigits > returns the label unchanged when there are no trailing digits
(pass) stripTrailingDigits > strips the trailing digit run
(pass) validateDomainLabel digit-suffix rule > accepts labels with no trailing digits
(pass) validateDomainLabel digit-suffix rule > accepts labels with exactly two trailing digits
(pass) validateDomainLabel digit-suffix rule > rejects labels with exactly one trailing digit
(pass) validateDomainLabel digit-suffix rule > rejects labels with three or more trailing digits
(pass) validateDomainLabel digit-suffix rule > rejects labels shorter than three characters
(pass) validateDomainLabel digit-suffix rule > rejects labels with uppercase characters
(pass) validateDomainLabel digit-suffix rule > rejects labels with leading or trailing hyphen
(pass) validateGovernanceLabel stem-length rule > accepts stems of five characters or fewer
(pass) validateGovernanceLabel stem-length rule > rejects stems longer than five characters
(pass) validateGovernanceLabel stem-length rule > measures the stem with trailing digits stripped
(pass) validateGovernanceLabel stem-length rule > does not apply the PopRules digit-suffix rule
(pass) validateGovernanceLabel is independent of the PopRules digit-suffix rule > accepts ONE trailing digit, which PopRules rejects outright
(pass) validateGovernanceLabel is independent of the PopRules digit-suffix rule > accepts THREE OR MORE trailing digits, which PopRules also rejects
(pass) validateGovernanceLabel is independent of the PopRules digit-suffix rule > accepts zero trailing digits
(pass) validateGovernanceLabel is independent of the PopRules digit-suffix rule > accepts exactly two trailing digits
(pass) validateGovernanceLabel is independent of the PopRules digit-suffix rule > the NORMAL path still enforces 0-or-2 trailing digits
(pass) validateGovernanceLabel canonical-label rules > rejects uppercase characters
(pass) validateGovernanceLabel canonical-label rules > rejects labels containing a dot
(pass) validateGovernanceLabel canonical-label rules > rejects leading or trailing hyphen
(pass) validateGovernanceLabel canonical-label rules > rejects labels shorter than three characters

::endgroup::

::group::tests/unit/utils/contractInteractions.test.ts:
(pass) deriveDomainNode > matches namehashUnder(tldNode, labelhash) [1.00ms]
(pass) deriveDomainNode > different TLDs yield different nodes for the same label [1.00ms]
(pass) deriveDomainTokenId > is uint256(node)
(pass) deriveDomainTokenId > known vector: getsome under .dot
(pass) isRevertFlag matches the EVM revert bit > flags=0n → false
(pass) isRevertFlag matches the EVM revert bit > flags=1n → true
(pass) isRevertFlag matches the EVM revert bit > flags=2n → false
(pass) isRevertFlag matches the EVM revert bit > flags=3n → true
(pass) buildRevertError > empty data returns the unmapped-origin hint
(pass) buildRevertError > known ABI selector decodes to the named error [2.00ms]
(pass) buildRevertError > unknown selector falls back to raw hex [1.00ms]
(pass) decodeContractRevertError > empty data includes operation context
(pass) decodeContractRevertError > non-empty data decodes against the ABI [1.00ms]

::endgroup::

::group::tests/unit/text/textJson.test.ts:
(pass) text view --help shows --json option [2.00ms]
(pass) text set --help shows --json option [1.00ms]
(pass) text set --json emits JSON error when both mnemonic and key-uri provided [2.00ms]

::endgroup::

::group::tests/unit/text/textHelp.test.ts:
(pass) text --help lists subcommands and auth options [2.00ms]
(pass) text view --help shows name and key arguments [2.00ms]
(pass) text set --help shows name, key, and value arguments [1.00ms]

::endgroup::

::group::tests/unit/account/accountHelp.test.ts:
(pass) account --help lists subcommands including is-mapped, is-whitelisted, whitelist [2.00ms]
(pass) account is-mapped --help shows address argument and --json [1.00ms]
(pass) account is-whitelisted --help shows address argument and --json [2.00ms]
(pass) account whitelist --help shows address argument, --remove, and --json [2.00ms]
(pass) account is alias works for is-mapped [2.00ms]
(pass) account iw alias works for is-whitelisted [1.00ms]

::endgroup::

::group::tests/unit/register/registrationManifest.test.ts:
(pass) registration manifest persistence > saves a record and reads it back by label [71.00ms]
(pass) registration manifest persistence > encrypts the secret at rest (never stored in plaintext) [56.00ms]
(pass) registration manifest persistence > decrypts the secret with the correct credential [111.00ms]
(pass) registration manifest persistence > rejects decryption with the wrong credential [120.00ms]
(pass) registration manifest persistence > lists records for the env + caller, newest commit first [113.00ms]
(pass) registration manifest persistence > isolates records by env and caller [120.00ms]
(pass) registration manifest persistence > deletes a record [56.00ms]
(pass) registration manifest persistence > returns empty when nothing is cached
(pass) registration manifest persistence > clear record selection scopes to a requested label [147.00ms]
(pass) registration manifest persistence > clear record selection errors when a requested label is missing [58.00ms]
(pass) resolveManifestCredential > prefers password, then mnemonic, then key URI
(pass) resolveManifestCredential > returns null when no credential is available
(pass) resolveManifestCredential > uses env mnemonic and key URI for registration cache encryption
(pass) TLD ingested from chain > resolveTldInfo reads the deployment TLD rather than assuming .dot
(pass) TLD ingested from chain > retries a transient read failure before resolving [301.00ms]
(pass) TLD ingested from chain > the immutable TLD is cached across calls
(pass) TLD ingested from chain > does not share the cache between clients that share a controller address
(pass) TLD ingested from chain > computeDomainTokenId derives the id under the chain TLD
(pass) TLD ingested from chain > formatDomainName uses the deployment TLD suffix
(pass) ensureDomainNotRegistered enforces the controller predicate > passes when available(label) is true, checking available not ownerOf
(pass) ensureDomainNotRegistered enforces the controller predicate > throws with the real TLD suffix when available(label) is false [1.00ms]

::endgroup::

::group::tests/unit/register/registerHelp.test.ts:
(pass) root help lists register command [2.00ms]
(pass) register help shows subcommands [2.00ms]
(pass) register domain help shows options [2.00ms]
(pass) register subname help shows options [2.00ms]
(pass) register domain parses reverse flag [2.00ms]
(pass) register domain parses governance flag [2.00ms]
(pass) register domain parses owner option [2.00ms]
(pass) register domain parses transfer with destination [2.00ms]
(pass) register domain parses account option [2.00ms]
(pass) register domain parses keystore-path option [2.00ms]
(pass) register domain parses password option [1.00ms]
(pass) register domain parses mnemonic option [2.00ms]
(pass) register domain parses key-uri option [2.00ms]
(pass) register domain parses commitment-buffer option [1.00ms]
(pass) register domain parses commitment-buffer alias --cb [2.00ms]
(pass) register subname parses name and parent [3.00ms]
(pass) register subname parses owner option [2.00ms]
(pass) getCommitmentBufferSeconds defaults to 6 when env is not set
(pass) getCommitmentBufferSeconds reads from DOTNS_COMMITMENT_BUFFER env variable
(pass) COMMITMENT_POLL_INTERVAL_MS is 2000
(pass) COMMITMENT_POLL_TIMEOUT_MS is 30000

::endgroup::

::group::tests/unit/register/registerJson.test.ts:
(pass) register domain --help shows --json option [3.00ms]
(pass) register subname --help shows --json option [1.00ms]
(pass) register domain --json emits JSON error when --transfer without --to [2.00ms]

::endgroup::

::group::tests/unit/register/registerOwnerConflicts.test.ts:
(pass) register domain rejects --owner with --transfer [2.00ms]
(pass) register domain rejects --owner with --reverse [2.00ms]
(pass) register domain rejects --owner with --governance [2.00ms]

::endgroup::

::group::tests/unit/cli/dotliViewUrls.test.ts:
(pass) paseo-v2 emits only its paseo.li gateway [1.00ms]
(pass) devnet emits only its dev-dot.li gateway
(pass) previewnet emits no dot.li gateway

::endgroup::

::group::tests/unit/cli/authOptions.test.ts:
(pass) getAuthOptions resolves a global --env given before a two-level-nested command [1.00ms]
(pass) getAuthOptions lets a command-level --env override the global one
(pass) getAuthOptions resolves the --network alias from the program root

::endgroup::

::group::tests/unit/cli/qrSigner.test.ts:
⚠ The QR mobile-wallet signer is experimental and still in development; expect rough edges.
(pass) createQrSigner > throws a clear error when no global WebSocket is available [2.00ms]
⚠ The QR mobile-wallet signer is experimental and still in development; expect rough edges.
Approve the signing allowance on your phone to continue.
(pass) createQrSigner > returns the SS58 origin derived from the paired signer's public key [2.00ms]

::endgroup::

::group::tests/unit/cli/transfer.test.ts:
(pass) resolveTransferRecipient > returns a raw EVM address in checksummed form without touching the chain [1.00ms]
(pass) resolveTransferRecipient > rejects input that is neither an EVM address, SS58 address, nor domain name

::endgroup::

::group::tests/unit/cli/signerKind.test.ts:
(pass) resolveSignerKind > defaults to keystore
(pass) resolveSignerKind > reads DOTNS_SIGNER=qr
(pass) resolveSignerKind > an explicit flag overrides the env var
(pass) resolveSignerKind > an unknown value falls back to keystore
(pass) resolveQrAppId > defaults to the unified product id
(pass) resolveQrAppId > flag beats env beats default
(pass) resolveQrPeopleEndpoints > defaults to the paseo stage
(pass) resolveQrPeopleEndpoints > resolves named stages
(pass) resolveQrPeopleEndpoints > treats a non-stage value as a comma-separated wss list
(pass) resolveQrPeopleEndpoints > rejects a non-wss URL
(pass) resolveQrPeopleEndpoints > rejects an empty/blank value
(pass) resolveQrPeopleEndpoints > rejects a wss URL with embedded credentials
(pass) resolveQrPeopleEndpoints > flag beats env beats default
(pass) assertSignerOptions > keystore signer allows local-account flags [1.00ms]
(pass) assertSignerOptions > qr signer with no local flags is allowed
(pass) assertSignerOptions > qr signer rejects a local-account flag
(pass) assertSignerOptions > qr signer lists every conflicting flag
(pass) assertSignerOptions > qr from env is also guarded
(pass) assertSignerOptions > qr signer rejects a keystore secret supplied via env

::endgroup::

::group::tests/unit/cli/bulletinRpc.test.ts:
(pass) paseo-v2 default resolves to the existing bulletin RPC
(pass) explicit argument wins over env var and active env
(pass) env var wins over active env config when no explicit argument
(pass) falls back to active env's bulletin RPC
(pass) throws when nothing is configured (synthetic case)
(pass) bulletin cache Asset Hub RPC uses merged CLI rpc before DOTNS_RPC
(pass) bulletin cache Asset Hub RPC uses selected environment when no rpc override exists
(pass) bulletin cache rejects custom Bulletin env override without matching target
(pass) bulletin cache accepts custom Bulletin override with explicit environment
(pass) bulletin cache custom Bulletin with DOTNS_ENV ignores stale DOTNS_RPC
(pass) bulletin cache accepts custom Bulletin override with explicit Asset Hub RPC

::endgroup::

::group::tests/unit/cli/paseoCreate3Addresses.test.ts:
(pass) paseo-v2 uses the canonical 420420417 deployment address book
(pass) previewnet shares the same canonical address book as paseo-v2
(pass) devnet keeps its own distinct deployment, not the shared book

::endgroup::

::group::tests/unit/cli/environment.test.ts:
(pass) defaults to paseo-v2
(pass) accepts friendly paseo-v2 aliases
(pass) DOTNS_ENV selects rpc and contract set
(pass) --env takes precedence over DOTNS_ENV while --rpc only overrides endpoint
(pass) printCommandHeader emits one contextual line with version, action, target and network

::endgroup::

 300 pass
 0 fail
 852 expect() calls
Ran 300 tests across 35 files. [4.06s]

@re-gius re-gius left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please also fix this

const viewUrls = dotliViewUrls(name);

On a non-.dot deployment, passing the fully-qualified name (e.g. alice.paseo) produces alice.paseo.paseo.li, because dotliViewUrls only strips .dot. Please pass the normalized label from the resolved result, or make dotliViewUrls TLD-aware.

Comment thread packages/cli/src/core/naming.ts Outdated
@sphamjoli
sphamjoli requested a review from re-gius August 19, 2026 06:44
Comment thread packages/cli/src/cli/commands/register.ts Outdated
const pending: CommitmentRecord[] = [];
for (const record of records) {
const registered = await step(`Checking ${record.label}.dot`, async () =>
const registered = await step(`Checking ${record.label}`, async () =>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly, here existing implementation has a record.label.domain, while we only use the record.label

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. This one has the session context, so I will format the full name here too. Thanks for spotting it.

console.log(
chalk.gray(" • ") +
chalk.cyan((row.label + ".dot").padEnd(24)) +
chalk.cyan(row.label.padEnd(24)) +

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing domain here as well??

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, missed this. It has the context as well, so I will show label.<tld>. Will fix.

throw new Error(`Domain ${label}.dot has no owner`);
// A name is a label plus at most one TLD segment. Reject anything else here so
// clearly-invalid input fails without a chain read for the TLD.
if (/^[a-z0-9-]+(\.[a-z0-9-]+)?$/.test(input.toLowerCase())) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ig this rejects any subdomains as well right? If so is this the intended scenario considering we expect recipient identifier?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it rejects subdomains, and that is on purpose. This branch looks up a name owner via registrar.ownerOf, and only second-level names are minted there; subdomains live in the registry and never had an owner to resolve. The old code rejected them too, via isLabelLike (which forbids dots). The regex just adds the label.tld form on top.

@@ -10,8 +11,9 @@ async function main() {
validateDomainLabel(label);

const cid = process.env.DOTNS_CID ?? "bafybeigdyr...replace_me";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the fallback value really required, if so in what situation?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is the placeholder CID, so the example runs without setting DOTNS_CID. It is not needed for correctness, you just override it with the env var. It predates this PR, so I left it as is.

const { ctx } = await connectDotns();

const name = process.env.DOTNS_NAME ?? "example.dot";
const name = process.env.DOTNS_NAME ?? "example";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would still have the expected Tld right? "example.theTld"

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. performDomainLookup takes the bare label and builds the full name from the chain TLD, so it resolves as example.<tld>. That is why I dropped the .dot from the default: it would be wrong on a non-dot chain.

pending = fetchTldInfo(ctx).catch((error) => {
// Do not cache failures: a transient read error must not poison later calls.
tldInfoCache.delete(key);
throw error;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not very imp but, is it worth retrying before we throw an error?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a light retry already, just not inline: on failure the cache entry is deleted, so the next call fetches again rather than reusing a bad result. Within a single call there is no retry, and read() has its own timeout. Happy to add a small bounded retry if you would prefer one.

@GHkrishna GHkrishna Aug 19, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ig should be fine, that way we also defer any unwanted overheads to make the call

Comment thread packages/cli/package.json
"name": "@parity/dotns-cli",
"module": "index.ts",
"version": "0.6.9",
"version": "0.8.3",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do we decide the version? Is this in sync with other packages??

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The version is out of sync it was an oversight for many versions

@sphamjoli
sphamjoli merged commit fada7ee into main Aug 19, 2026
16 of 17 checks passed
@sphamjoli
sphamjoli deleted the spha/dynamic-tld branch August 19, 2026 08:42
filip-parity added a commit that referenced this pull request Sep 2, 2026
…g nodes (#277)

#### Description

`lookup` hashed user input as-is, so a fully-qualified name
(`alice.paseo`) derived the wrong node/tokenId, reported a registered
name as "not registered", and displayed a doubled suffix
(`alice.paseo.paseo`) — while every write path (register, transfer,
delegate, escrow, records) already normalised input through
`normaliseName` (chain-resolved TLD, from #261).

This PR normalises both lookup entry points the same way:

- `performDomainLookup` — normalises before
`formatDomainName`/`domainNode` (also fixes the trailing-digit base-name
check running on the suffixed string)
- `performOwnerOfLookup` — normalises instead of only trimming
- `lookup transfer` — normalises its display label and JSON `label`
output (the underlying `transferName`/`verifyDomainOwnership` already
normalised)

#### Testing

How to test:

1. `bun test packages/cli/tests/unit/lookup/` — new tests pin that
`alice` and `alice.<tld>` derive the same node/tokenId against a mocked
chain, and that a foreign suffix (`alice.dot` on a `.paseo` deployment)
is not stripped.
2. Live: `bun run src/cli/index.ts lookup owner-of <name>.paseo --env
paseo-v2` — now prints `Domain: <name>.paseo` (was `<name>.paseo.paseo`)
and reads the correct token. All 304 unit tests pass.

#### Notes

Verified on paseo-v2 (previewnet's shared dev account currently has no
balance/mapping after the chain relaunch, so it cannot serve as a live
testbed until refunded).

---------

Co-authored-by: Giuseppe Re <giuseppe.re@parity.io>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants