bulletin-deploy 0.18.0 → 0.18.2-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/bin/bulletin-deploy +4 -1
  2. package/dist/auth-config.js +3 -3
  3. package/dist/bug-report.js +4 -4
  4. package/dist/chain-health.js +3 -3
  5. package/dist/{chunk-A7GZZQ3Q.js → chunk-6EGOJ4VS.js} +2 -2
  6. package/dist/{chunk-6LR7OM7F.js → chunk-AAJ7QAAM.js} +35 -15
  7. package/dist/{chunk-WYJX7OFF.js → chunk-D3NJT3DW.js} +1 -1
  8. package/dist/chunk-HNJWIY55.js +8 -0
  9. package/dist/{chunk-TIQ3TCK4.js → chunk-JBWL6PA4.js} +3 -3
  10. package/dist/{chunk-RSAEMNX4.js → chunk-MG7JEKB7.js} +230 -27
  11. package/dist/{chunk-6WLVGBLQ.js → chunk-MMKZKXSS.js} +1 -1
  12. package/dist/{chunk-3CES4RZG.js → chunk-NXZHFDRN.js} +1 -1
  13. package/dist/{chunk-6BIAW73W.js → chunk-QSE3WU2B.js} +1 -1
  14. package/dist/{chunk-LZ7HCV4C.js → chunk-RDJSG2D3.js} +1 -1
  15. package/dist/{chunk-3TDAKT7Y.js → chunk-S4ZROJ5B.js} +3 -3
  16. package/dist/{chunk-F4OW5UVA.js → chunk-WFFDZKW7.js} +1 -1
  17. package/dist/{chunk-UY2DR5V4.js → chunk-ZP4U7C3N.js} +3 -3
  18. package/dist/chunk-probe.js +3 -3
  19. package/dist/commands/login.js +12 -11
  20. package/dist/commands/logout.js +4 -4
  21. package/dist/commands/transfer.js +4 -4
  22. package/dist/commands/whoami.js +3 -3
  23. package/dist/deploy-actors.js +6 -6
  24. package/dist/deploy.js +12 -11
  25. package/dist/dotns.d.ts +98 -6
  26. package/dist/dotns.js +4 -4
  27. package/dist/index.js +13 -12
  28. package/dist/manifest/publish.js +13 -12
  29. package/dist/manifest/schema.d.ts +4 -0
  30. package/dist/memory-report.js +2 -2
  31. package/dist/merkle.js +12 -11
  32. package/dist/personhood/bootstrap.js +13 -13
  33. package/dist/personhood/people-client.js +4 -4
  34. package/dist/run-state.js +1 -1
  35. package/dist/sss-allowance-cache.js +4 -4
  36. package/dist/storage-signer.js +12 -11
  37. package/dist/subname-depth.d.ts +22 -0
  38. package/dist/subname-depth.js +6 -0
  39. package/dist/telemetry.js +2 -2
  40. package/dist/version-check.js +3 -3
  41. package/package.json +3 -3
  42. package/tools/release-retry-wrapper.mjs +11 -0
@@ -585,8 +585,11 @@ try {
585
585
  console.error(`Manifest publish failed: ${err?.message ?? err}`);
586
586
  process.exit(err instanceof NonRetryableError ? EXIT_CODE_NO_RETRY : 1);
587
587
  }
588
+ } else if (!loaded && !flags.noManifest) {
589
+ // Only the walk-up can come back empty: an explicit --config that is
590
+ // missing throws out of tryLoadProductConfig rather than returning null.
591
+ console.log(`No product config found from ${buildDirAbs} upward; content deployed, no manifest published.`);
588
592
  }
589
- // No config (or --no-manifest) → the contenthash-only deploy above is the whole job; nothing else to do.
590
593
  }
591
594
 
592
595
  if (!flags.help && !flags.version) {
@@ -9,12 +9,12 @@ import {
9
9
  getPeopleChainEndpoints,
10
10
  hasPersistedSession,
11
11
  resolveBulletinEndpoints
12
- } from "./chunk-6WLVGBLQ.js";
12
+ } from "./chunk-MMKZKXSS.js";
13
13
  import "./chunk-TSPERKUS.js";
14
14
  import "./chunk-HDGD5FYX.js";
15
15
  import "./chunk-ZOC4GITL.js";
16
- import "./chunk-3CES4RZG.js";
17
- import "./chunk-3TDAKT7Y.js";
16
+ import "./chunk-NXZHFDRN.js";
17
+ import "./chunk-S4ZROJ5B.js";
18
18
  export {
19
19
  DOT_DAPP_ID,
20
20
  DOT_DERIVATION_INDEX,
@@ -10,10 +10,10 @@ import {
10
10
  offerBugReport,
11
11
  scrubSecrets,
12
12
  setDeployContext
13
- } from "./chunk-UY2DR5V4.js";
14
- import "./chunk-F4OW5UVA.js";
15
- import "./chunk-3CES4RZG.js";
16
- import "./chunk-3TDAKT7Y.js";
13
+ } from "./chunk-ZP4U7C3N.js";
14
+ import "./chunk-WFFDZKW7.js";
15
+ import "./chunk-NXZHFDRN.js";
16
+ import "./chunk-S4ZROJ5B.js";
17
17
  export {
18
18
  buildCliFlagsSummary,
19
19
  buildLabels,
@@ -3,9 +3,9 @@ import {
3
3
  DEFAULT_WINDOW_BLOCKS,
4
4
  MAX_SPANS_PER_SAMPLER,
5
5
  endpointHost
6
- } from "./chunk-LZ7HCV4C.js";
7
- import "./chunk-3CES4RZG.js";
8
- import "./chunk-3TDAKT7Y.js";
6
+ } from "./chunk-RDJSG2D3.js";
7
+ import "./chunk-NXZHFDRN.js";
8
+ import "./chunk-S4ZROJ5B.js";
9
9
  export {
10
10
  ChainHealthSampler,
11
11
  DEFAULT_WINDOW_BLOCKS,
@@ -4,10 +4,10 @@ import {
4
4
  } from "./chunk-5FLTDWWP.js";
5
5
  import {
6
6
  DEFAULT_MNEMONIC
7
- } from "./chunk-RSAEMNX4.js";
7
+ } from "./chunk-MG7JEKB7.js";
8
8
  import {
9
9
  DOT_PRODUCT_ID
10
- } from "./chunk-6WLVGBLQ.js";
10
+ } from "./chunk-MMKZKXSS.js";
11
11
 
12
12
  // src/deploy-actors.ts
13
13
  var DEFAULT_WORKER_SURI = DEFAULT_MNEMONIC;
@@ -1,9 +1,12 @@
1
+ import {
2
+ subnameNestingLevels
3
+ } from "./chunk-HNJWIY55.js";
1
4
  import {
2
5
  deriveProductSigner
3
6
  } from "./chunk-M5AKL3FL.js";
4
7
  import {
5
8
  preflightSssAllowance
6
- } from "./chunk-6BIAW73W.js";
9
+ } from "./chunk-QSE3WU2B.js";
7
10
  import {
8
11
  statementSigningAccount
9
12
  } from "./chunk-GRPLHUYC.js";
@@ -29,18 +32,18 @@ import {
29
32
  } from "./chunk-WPC2PUCV.js";
30
33
  import {
31
34
  setDeployContext
32
- } from "./chunk-UY2DR5V4.js";
35
+ } from "./chunk-ZP4U7C3N.js";
33
36
  import {
34
37
  getBestBlockNumber,
35
38
  probeChunks,
36
39
  probeFinalityGap
37
- } from "./chunk-WYJX7OFF.js";
40
+ } from "./chunk-D3NJT3DW.js";
38
41
  import {
39
42
  packSection
40
43
  } from "./chunk-C2TS5MER.js";
41
44
  import {
42
45
  resolveStorageSigner
43
- } from "./chunk-A7GZZQ3Q.js";
46
+ } from "./chunk-6EGOJ4VS.js";
44
47
  import {
45
48
  createSlotAccountSigner,
46
49
  requestResourceAllocation
@@ -50,12 +53,13 @@ import {
50
53
  DotNS,
51
54
  TX_TIMEOUT_MS,
52
55
  classifyRegistrability,
56
+ computeDomainNode,
53
57
  fetchNonce,
54
58
  formatUnregistrableReason,
55
59
  parseDomainName,
56
60
  popStatusName,
57
61
  verifyNonceAdvanced
58
- } from "./chunk-RSAEMNX4.js";
62
+ } from "./chunk-MG7JEKB7.js";
59
63
  import {
60
64
  derivePoolAccounts,
61
65
  detectTestnet,
@@ -69,7 +73,7 @@ import {
69
73
  STALE_SESSION_MESSAGE,
70
74
  getPeopleChainEndpoints,
71
75
  hasPersistedSession
72
- } from "./chunk-6WLVGBLQ.js";
76
+ } from "./chunk-MMKZKXSS.js";
73
77
  import {
74
78
  CLI_NAME
75
79
  } from "./chunk-TSPERKUS.js";
@@ -84,7 +88,7 @@ import {
84
88
  } from "./chunk-ZOC4GITL.js";
85
89
  import {
86
90
  ChainHealthSampler
87
- } from "./chunk-LZ7HCV4C.js";
91
+ } from "./chunk-RDJSG2D3.js";
88
92
  import {
89
93
  VERSION,
90
94
  captureWarning,
@@ -98,7 +102,7 @@ import {
98
102
  truncateAddress,
99
103
  withDeploySpan,
100
104
  withSpan
101
- } from "./chunk-3CES4RZG.js";
105
+ } from "./chunk-NXZHFDRN.js";
102
106
 
103
107
  // src/merkle.ts
104
108
  import * as fs2 from "fs";
@@ -2025,11 +2029,18 @@ function assertSubdomainOwnerMatchesSigner(result, signerEvmAddress, sublabel, p
2025
2029
  }
2026
2030
  }
2027
2031
  function formatSubdomainParentError(fullName, parentLabel, parentOwner, selfAddress, tld = DEFAULT_TLD, profile = "poprules-startingPrice") {
2028
- if (parentOwner !== null) {
2029
- const transferBullet = selfAddress ? ` - ask the ${parentOwner} account to hand the parent to this signer, run BY that account: ${CLI_NAME} transfer ${parentLabel}.${tld} --to ${selfAddress} --mnemonic <the ${parentOwner} account's key>
2032
+ const owner = parentOwner !== null && /^0x0{40}$/i.test(parentOwner) ? null : parentOwner;
2033
+ const parentIsNested = parentLabel.includes(".");
2034
+ if (owner === null && parentIsNested) {
2035
+ return `Cannot deploy ${fullName}: parent ${parentLabel}.${tld} does not exist.
2036
+
2037
+ It is itself a subname, so it cannot be registered directly \u2014 whoever owns ${parentLabel.slice(parentLabel.indexOf(".") + 1)}.${tld} has to create it first, then hand it to this signer (${selfAddress || "this account"}).`;
2038
+ }
2039
+ if (owner !== null) {
2040
+ const transferBullet = selfAddress ? ` - ask the ${owner} account to hand the parent to this signer, run BY that account: ${CLI_NAME} transfer ${parentLabel}.${tld} --to ${selfAddress} --mnemonic <the ${owner} account's key>
2030
2041
  ` : "";
2031
2042
  const ownedParentBullet = ` - deploy the subdomain under a parent this signer already owns instead.`;
2032
- return `Cannot deploy ${fullName}: parent ${parentLabel}.${tld} is owned by ${parentOwner}, not by this signer.
2043
+ return `Cannot deploy ${fullName}: parent ${parentLabel}.${tld} is owned by ${owner}, not by this signer.
2033
2044
 
2034
2045
  ` + (transferBullet ? `Either:
2035
2046
  ${transferBullet}${ownedParentBullet}` : ownedParentBullet);
@@ -2158,6 +2169,10 @@ async function deploy(content, domainName = null, options = {}) {
2158
2169
  _deployRpcFailedOver = false;
2159
2170
  POOL_SIZE = options.poolSize ?? parseInt(process.env.BULLETIN_POOL_SIZE ?? String(DEFAULT_POOL_SIZE), 10);
2160
2171
  let parsed = domainName ? parseDomainName(domainName, envTld) : null;
2172
+ if (parsed?.isSubdomain && parsed.parentLabel.includes(".")) {
2173
+ const levels = subnameNestingLevels(1 + parsed.parentLabel.split(".").length);
2174
+ console.log(` NOTE: "${domainName}" parses as sublabel "${parsed.sublabel}" under parent path "${parsed.parentLabel}.${envTld}" (${levels} levels of subname nesting).`);
2175
+ }
2161
2176
  let sessionCleanup;
2162
2177
  const hasSession = hasPersistedSession();
2163
2178
  const signerChoice = chooseSignerInput({
@@ -2264,6 +2279,9 @@ async function deploy(content, domainName = null, options = {}) {
2264
2279
  setDeployAttribute("deploy.env", envId);
2265
2280
  setDeployAttribute("deploy.label", parsed?.label ?? name);
2266
2281
  setDeployAttribute("deploy.subdomain", String(parsed?.isSubdomain ?? false));
2282
+ if (parsed?.isSubdomain) {
2283
+ setDeployAttribute("deploy.dotns.subname_levels", subnameNestingLevels(1 + parsed.parentLabel.split(".").length));
2284
+ }
2267
2285
  if (envNetwork) setDeployAttribute("deploy.network", envNetwork);
2268
2286
  if (envSource) setDeployAttribute("deploy.environments_source", envSource);
2269
2287
  setDeployAttribute("deploy.transfer.enabled", options.transferTo ? "true" : "false");
@@ -2296,8 +2314,9 @@ async function deploy(content, domainName = null, options = {}) {
2296
2314
  const subResult = await preflight.checkSubdomainOwnership(parsed.sublabel, parsed.parentLabel);
2297
2315
  assertSubdomainOwnerMatchesSigner(subResult, preflight.evmAddress, parsed.sublabel, parsed.parentLabel, envTld);
2298
2316
  if (!subResult.owned) {
2299
- const { owned: parentOwned, owner: parentOwner } = await preflight.checkOwnership(parsed.parentLabel);
2300
- if (!parentOwned) {
2317
+ const parentNode = computeDomainNode(parsed.parentLabel, envTld);
2318
+ const { authorised: parentAuthorised, owner: parentOwner } = await preflight.checkNodeAuthorization(parentNode, preflight.evmAddress);
2319
+ if (!parentAuthorised) {
2301
2320
  throw new NonRetryableError(
2302
2321
  formatSubdomainParentError(parsed.fullName, parsed.parentLabel, parentOwner ?? null, preflight.evmAddress ?? "", envTld, preflight.protocolVersion)
2303
2322
  );
@@ -2554,8 +2573,9 @@ async function deploy(content, domainName = null, options = {}) {
2554
2573
  } else if (owner) {
2555
2574
  throw new Error(`Subdomain ${parsed.fullName} is owned by ${owner}, not ${dotns.evmAddress}`);
2556
2575
  } else {
2557
- const parentOwnership = await dotns.checkOwnership(parsed.parentLabel);
2558
- if (!parentOwnership.owned) throw new Error(`You must own ${parsed.parentLabel}.${envTld} to register subdomains under it`);
2576
+ const parentNode = computeDomainNode(parsed.parentLabel, envTld);
2577
+ const { authorised: parentAuthorised } = await dotns.checkNodeAuthorization(parentNode, dotns.evmAddress);
2578
+ if (!parentAuthorised) throw new Error(`You must own (or be approved for) ${parsed.parentLabel}.${envTld} to register subdomains under it`);
2559
2579
  console.log(` Status: Registering subdomain...`);
2560
2580
  await dotns.registerSubdomain(parsed.sublabel, parsed.parentLabel);
2561
2581
  registeredFresh = true;
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  captureWarning
3
- } from "./chunk-3CES4RZG.js";
3
+ } from "./chunk-NXZHFDRN.js";
4
4
 
5
5
  // src/chunk-probe.ts
6
6
  import { Twox128, Blake2128Concat, decAnyMetadata, unifyMetadata } from "@polkadot-api/substrate-bindings";
@@ -0,0 +1,8 @@
1
+ // src/subname-depth.ts
2
+ function subnameNestingLevels(labelsBeforeTld) {
3
+ return labelsBeforeTld - 1;
4
+ }
5
+
6
+ export {
7
+ subnameNestingLevels
8
+ };
@@ -13,13 +13,13 @@ import {
13
13
  setBulletinEndpoints,
14
14
  storeDirectory,
15
15
  storeFile
16
- } from "./chunk-6LR7OM7F.js";
16
+ } from "./chunk-AAJ7QAAM.js";
17
17
  import {
18
18
  DEFAULT_TLD,
19
19
  DotNS,
20
20
  TX_KIND_SKIPPED,
21
21
  stripTldSuffix
22
- } from "./chunk-RSAEMNX4.js";
22
+ } from "./chunk-MG7JEKB7.js";
23
23
  import {
24
24
  DEFAULT_ENV_ID,
25
25
  getPopSelfServeConfig,
@@ -33,7 +33,7 @@ import {
33
33
  setDeployAttribute,
34
34
  withManifestSpan,
35
35
  withSpan
36
- } from "./chunk-3CES4RZG.js";
36
+ } from "./chunk-NXZHFDRN.js";
37
37
 
38
38
  // src/manifest/publish.ts
39
39
  import * as fs2 from "fs/promises";
@@ -13,7 +13,7 @@ import {
13
13
  } from "./chunk-ZOC4GITL.js";
14
14
  import {
15
15
  ChainHealthSampler
16
- } from "./chunk-LZ7HCV4C.js";
16
+ } from "./chunk-RDJSG2D3.js";
17
17
  import {
18
18
  captureWarning,
19
19
  markCodePath,
@@ -21,7 +21,7 @@ import {
21
21
  setDeploySentryTag,
22
22
  truncateAddress,
23
23
  withSpan
24
- } from "./chunk-3CES4RZG.js";
24
+ } from "./chunk-NXZHFDRN.js";
25
25
  import {
26
26
  POP_CONTROLLER_PROBE_ABI,
27
27
  PROTOCOL_PROBE_LABEL,
@@ -411,7 +411,21 @@ var DOTNS_REGISTRY_ABI = [
411
411
  { inputs: [{ name: "record", type: "tuple", components: [{ name: "parentNode", type: "bytes32" }, { name: "subLabel", type: "string" }, { name: "parentLabel", type: "string" }, { name: "owner", type: "address" }] }], name: "setSubnodeOwner", outputs: [{ name: "subnode", type: "bytes32" }], stateMutability: "nonpayable", type: "function" },
412
412
  { inputs: [{ name: "node", type: "bytes32" }, { name: "newResolver", type: "address" }], name: "setResolver", outputs: [], stateMutability: "nonpayable", type: "function" },
413
413
  { inputs: [{ name: "node", type: "bytes32" }], name: "owner", outputs: [{ name: "", type: "address" }], stateMutability: "view", type: "function" },
414
- { inputs: [{ name: "node", type: "bytes32" }], name: "resolver", outputs: [{ name: "", type: "address" }], stateMutability: "view", type: "function" }
414
+ { inputs: [{ name: "node", type: "bytes32" }], name: "resolver", outputs: [{ name: "", type: "address" }], stateMutability: "view", type: "function" },
415
+ // #1435-adjacent: DotnsRegistry.isAuthorised(node, account) — verified against
416
+ // upstream IDotnsRegistry.sol at v0.7.0. The contract's own doc comment calls
417
+ // this "the canonical authorisation check the registry enforces on
418
+ // owner-gated entry points" and "the single source of truth" _authorised/
419
+ // isAuthorised both delegate to: a non-zero stored subnode owner must equal
420
+ // account; otherwise, for a tokenised node, the registrar's ERC-721 owner,
421
+ // an operator-for-all delegate, or a single-token approval all qualify.
422
+ // Strictly wider than a raw owner()-equality test. See
423
+ // checkNodeAuthorization below, which prefers this and falls back to
424
+ // owner()-equality only when this accessor is absent (a bare revert).
425
+ { inputs: [{ name: "node", type: "bytes32" }, { name: "account", type: "address" }], name: "isAuthorised", outputs: [{ name: "authorisedFlag", type: "bool" }], stateMutability: "view", type: "function" }
426
+ ];
427
+ var DOTNS_REGISTRY_SET_SUBNODE_OWNER_ABI_V07 = [
428
+ { inputs: [{ name: "record", type: "tuple", components: [{ name: "parentNode", type: "bytes32" }, { name: "subLabel", type: "string" }, { name: "parentLabel", type: "string" }, { name: "owner", type: "address" }, { name: "persist", type: "bool" }] }], name: "setSubnodeOwner", outputs: [{ name: "subnode", type: "bytes32" }], stateMutability: "nonpayable", type: "function" }
415
429
  ];
416
430
  var DOTNS_CONTENT_RESOLVER_ABI = [
417
431
  { inputs: [{ name: "node", type: "bytes32" }, { name: "hash", type: "bytes" }], name: "setContenthash", outputs: [], stateMutability: "nonpayable", type: "function" },
@@ -482,6 +496,9 @@ function dotnsContractName(address, contracts = CONTRACTS) {
482
496
  function stringifyDebugValue(value) {
483
497
  return JSON.stringify(value, (_key, nested) => typeof nested === "bigint" ? nested.toString() : nested);
484
498
  }
499
+ function isBareRevertResult(revertData, revertFlags) {
500
+ return (revertData === void 0 || revertData.trim() === "0x") && revertFlags === 1n;
501
+ }
485
502
  function dotnsTxDebugEnabled() {
486
503
  return process.env.BULLETIN_DEPLOY_DOTNS_DEBUG === "1" || process.env.DOTNS_DEBUG === "1";
487
504
  }
@@ -510,8 +527,7 @@ function formatContractDryRunFailure(gasEstimate, context) {
510
527
  lines.push(` calldata: ${context.encodedData}`);
511
528
  if (context.args) lines.push(` args: ${stringifyDebugValue(context.args)}`);
512
529
  }
513
- const revertData = gasEstimate.revertData;
514
- const isBareRevert = (revertData === void 0 || revertData.trim() === "0x") && gasEstimate.revertFlags === 1n;
530
+ const isBareRevert = isBareRevertResult(gasEstimate.revertData, gasEstimate.revertFlags);
515
531
  if (isBareRevert && BARE_REVERT_DIAGNOSTIC_FUNCTIONS.has(functionName)) {
516
532
  if (functionName === "register") {
517
533
  lines.push(
@@ -814,13 +830,11 @@ function parseDomainName(input, tld = DEFAULT_TLD) {
814
830
  const sanitized = validateDomainLabel(parts[0]);
815
831
  return { isSubdomain: false, label: sanitized, sublabel: null, parentLabel: null, fullName: `${sanitized}.${tld}` };
816
832
  }
817
- if (parts.length === 2) {
818
- const sanitizedSub = validateDomainLabel(parts[0]);
819
- const sanitizedParent = validateDomainLabel(parts[1]);
820
- const fullLabel = `${sanitizedSub}.${sanitizedParent}`;
821
- return { isSubdomain: true, label: fullLabel, sublabel: sanitizedSub, parentLabel: sanitizedParent, fullName: `${fullLabel}.${tld}` };
822
- }
823
- throw new Error(`Invalid domain: only one level of subdomains supported (got ${parts.length} labels)`);
833
+ const restParts = parts.slice(1);
834
+ const sanitizedSub = validateDomainLabel(parts[0]);
835
+ const sanitizedParent = restParts.map((p) => validateDomainLabel(p)).join(".");
836
+ const fullLabel = `${sanitizedSub}.${sanitizedParent}`;
837
+ return { isSubdomain: true, label: fullLabel, sublabel: sanitizedSub, parentLabel: sanitizedParent, fullName: `${fullLabel}.${tld}` };
824
838
  }
825
839
  function parseProofOfPersonhoodStatus(status) {
826
840
  const s = (status ?? "none").toLowerCase();
@@ -1322,6 +1336,22 @@ var DotNS = class _DotNS {
1322
1336
  // this into a throw-if-unset without re-checking both of those.
1323
1337
  _protocolVersion = DEFAULT_DOTNS_PROFILE;
1324
1338
  _adapter = getAdapter(DEFAULT_DOTNS_PROFILE);
1339
+ // #1435: which setSubnodeOwner tuple shape this connection's chain accepts
1340
+ // — "legacy" (4-field, pre-v0.7) or "v07" (5-field + persist). `null` means
1341
+ // "not yet probed"; resolveSubnodeOwnerShape resolves it lazily on the
1342
+ // FIRST subname write per connection (register or transfer, whichever runs
1343
+ // first) and caches the verdict here for the rest of that connection's
1344
+ // life. Defaults to "legacy" (NOT null) for the exact same reason
1345
+ // _protocolVersion defaults to DEFAULT_DOTNS_PROFILE above: every existing
1346
+ // test/library caller that constructs a DotNS instance and stubs
1347
+ // contractTransaction/submitBatchedContractCalls directly WITHOUT ever
1348
+ // calling connect() must keep today's exact pre-#1435 4-field behaviour,
1349
+ // with no probe attempted at all (there is no clientWrapper to probe
1350
+ // against in that case). connect() resets this to null so a real
1351
+ // connection always re-probes on its first subname write rather than
1352
+ // trusting this default or a previous connection's stale verdict — see
1353
+ // the reset next to detectProtocolVersion() below.
1354
+ _subnodeOwnerShape = "legacy";
1325
1355
  _onPhoneSigningRequired = void 0;
1326
1356
  _confirmPhoneReady = void 0;
1327
1357
  /** Total phone-signature count for this DotNS session (drives the `total` field passed to confirmPhoneReady). */
@@ -1359,6 +1389,10 @@ var DotNS = class _DotNS {
1359
1389
  this._protocolVersion = profile;
1360
1390
  this._adapter = getAdapter(profile);
1361
1391
  }
1392
+ /** #1435 test-only: pin the setSubnodeOwner shape cache directly ("legacy", "v07", or null to force resolveSubnodeOwnerShape to re-probe), bypassing the live dry-run probe — for unit tests that stub the probe or exercise the fallback/caching logic itself. */
1393
+ __setSubnodeOwnerShapeForTest(shape) {
1394
+ this._subnodeOwnerShape = shape;
1395
+ }
1362
1396
  constructor() {
1363
1397
  this.client = null;
1364
1398
  this.clientWrapper = null;
@@ -1540,6 +1574,7 @@ var DotNS = class _DotNS {
1540
1574
  this.connected = true;
1541
1575
  await this.resolveAndVerifyTld(options.tld);
1542
1576
  await this.detectProtocolVersion();
1577
+ this._subnodeOwnerShape = null;
1543
1578
  if (options.dotnsProtocol && options.dotnsProtocol !== this._protocolVersion) {
1544
1579
  this.connected = false;
1545
1580
  throw new Error(
@@ -1932,8 +1967,13 @@ var DotNS = class _DotNS {
1932
1967
  * v0.5.8-rc1 (pricingVersion answers on both), and classifyProtocolVersion
1933
1968
  * already only consults it when pricingVersionOk is true, so gating it
1934
1969
  * behind that result resolving first would just add a serialized
1935
- * round-trip for no benefit — worse once v0.6.0 is live everywhere (#1410),
1936
- * since that "no benefit" case becomes the common one. Resolves to `null`
1970
+ * round-trip for no benefit — worse as v0.6.0 spreads (#1410), since that
1971
+ * "no benefit" case becomes the common one. The discarded-probe cost does
1972
+ * NOT self-liquidate the moment v0.6.0 ships: it persists on every
1973
+ * environment still running an older generation (dotns-protocol.ts's
1974
+ * profile comment owns per-generation status — this one does not restate
1975
+ * it). Parallel regardless: one discarded read beats a serialized
1976
+ * round-trip on every connect. Resolves to `null`
1937
1977
  * immediately (no RPC at all) when DOTNS_POP_CONTROLLER has no configured
1938
1978
  * address.
1939
1979
  */
@@ -2086,6 +2126,92 @@ var DotNS = class _DotNS {
2086
2126
  }
2087
2127
  throw new Error("contractTransaction: unreachable \u2014 phone-silence re-arm loop exited without resolving");
2088
2128
  }
2129
+ /**
2130
+ * #1435: resolve — once per connection — which `setSubnodeOwner` tuple
2131
+ * shape this chain's DOTNS_REGISTRY accepts, and cache the verdict on
2132
+ * `_subnodeOwnerShape` for the rest of this connection's life. Called by
2133
+ * buildSetSubnodeOwnerCall on the first subname write (register or
2134
+ * transfer, whichever runs first); every subsequent subname write in the
2135
+ * same connection reuses the cached shape with no further chain I/O.
2136
+ *
2137
+ * There is no read-only capability probe for this (see
2138
+ * DOTNS_REGISTRY_SET_SUBNODE_OWNER_ABI_V07's comment — v0.7 adds no new
2139
+ * function/contract to probe), so this probes the WRITE itself: dry-run
2140
+ * the v0.7 (5-field, `persist: false`) encoding against `sampleRecord`
2141
+ * first (a dry run commits no state, so this never spends a fee or
2142
+ * mutates the registry). Three outcomes:
2143
+ *
2144
+ * - The dry run succeeds → this chain is on v0.7+; cache "v07".
2145
+ * - The dry run bare-reverts (empty `0x` data, flags=1 — i.e. no
2146
+ * function matches this selector at all) → this chain predates v0.7;
2147
+ * cache "legacy" and fall back to the 4-field encoding.
2148
+ * - The dry run reverts WITH data → the 5-field function EXISTS and
2149
+ * rejected the call for a real reason (bad parent, not authorised,
2150
+ * etc.) — that is a genuine failure of the caller's actual write, not
2151
+ * a shape mismatch, so it propagates immediately rather than being
2152
+ * swallowed into a fallback attempt. Conflating the two here would
2153
+ * turn a real authorization failure into a confusing shape-probe
2154
+ * loop, retrying the legacy shape (which will ALSO fail, just with a
2155
+ * less informative bare-revert-style error) instead of surfacing the
2156
+ * real cause.
2157
+ *
2158
+ * `sampleRecord` is the actual record the caller is about to write (not a
2159
+ * throwaway probe payload) — probing with real args means a v0.7 chain
2160
+ * that genuinely rejects this specific write (e.g. the signer doesn't own
2161
+ * the parent) reports that real reason here, on the first (probe) dry
2162
+ * run, rather than surfacing a second time once the real submission's own
2163
+ * internal dry run repeats the same call.
2164
+ */
2165
+ async resolveSubnodeOwnerShape(sampleRecord) {
2166
+ if (this._subnodeOwnerShape !== null) return this._subnodeOwnerShape;
2167
+ this.ensureConnected();
2168
+ if (!this.clientWrapper) throw new Error("resolveSubnodeOwnerShape: polkadot-api client not available");
2169
+ const contractAddress = this._contracts.DOTNS_REGISTRY;
2170
+ const v07Abi = DOTNS_REGISTRY_SET_SUBNODE_OWNER_ABI_V07;
2171
+ const v07Args = [{ ...sampleRecord, persist: false }];
2172
+ const encodedCallData = encodeFunctionData({ abi: v07Abi, functionName: "setSubnodeOwner", args: v07Args });
2173
+ const callResult = await this.clientWrapper.performDryRunCall(this.substrateAddress, contractAddress, 0n, encodedCallData);
2174
+ if (callResult.result.isOk) {
2175
+ this._subnodeOwnerShape = "v07";
2176
+ return "v07";
2177
+ }
2178
+ const errorData = callResult.result.value;
2179
+ const revertData = errorData?.data;
2180
+ const revertFlags = errorData?.flags;
2181
+ if (!isBareRevertResult(revertData, revertFlags)) {
2182
+ throw new Error(formatContractDryRunFailure({
2183
+ revertData,
2184
+ revertFlags,
2185
+ gasConsumed: callResult.gasConsumed,
2186
+ gasRequired: callResult.gasRequired,
2187
+ storageDeposit: callResult.storageDeposit?.value
2188
+ }, {
2189
+ contractAddress,
2190
+ functionName: "setSubnodeOwner",
2191
+ signerSubstrateAddress: this.substrateAddress,
2192
+ signerEvmAddress: this.evmAddress ?? void 0,
2193
+ value: 0n,
2194
+ encodedData: encodedCallData,
2195
+ args: v07Args,
2196
+ contracts: this._contracts
2197
+ }));
2198
+ }
2199
+ this._subnodeOwnerShape = "legacy";
2200
+ return "legacy";
2201
+ }
2202
+ /**
2203
+ * Build the `{ abi, args }` pair a setSubnodeOwner call site should submit,
2204
+ * after resolving (and caching) this connection's accepted tuple shape —
2205
+ * see resolveSubnodeOwnerShape. `record` carries the 4 fields common to
2206
+ * both shapes; the v0.7 shape appends `persist: false` (per #1435, this
2207
+ * only gates the LabelStore write, which is restricted to protocol store
2208
+ * writers — the ownership/resolver record itself is written either way).
2209
+ */
2210
+ async buildSetSubnodeOwnerCall(record) {
2211
+ const shape = await this.resolveSubnodeOwnerShape(record);
2212
+ if (shape === "v07") return { abi: DOTNS_REGISTRY_SET_SUBNODE_OWNER_ABI_V07, args: [{ ...record, persist: false }] };
2213
+ return { abi: DOTNS_REGISTRY_ABI, args: [record] };
2214
+ }
2089
2215
  async checkOwnership(label, ownerAddress = null) {
2090
2216
  this.ensureConnected();
2091
2217
  const checkAddress = (ownerAddress || this.evmAddress).toLowerCase();
@@ -2099,6 +2225,82 @@ var DotNS = class _DotNS {
2099
2225
  return { owned: false, owner: null };
2100
2226
  }
2101
2227
  }
2228
+ /**
2229
+ * Raw probe for DotnsRegistry.isAuthorised(node, account) — returns the
2230
+ * decoded bool when the accessor exists, or `null` when it is absent from
2231
+ * this registry deployment (a bare selector-not-found revert). Shares the
2232
+ * bare-revert-vs-real-revert discrimination (isBareRevertResult) with
2233
+ * resolveSubnodeOwnerShape's v0.7 shape probe above: a revert WITH data
2234
+ * means the function exists and the on-chain call itself is a normal view
2235
+ * read that should never revert for a real reason (see _isAuthorised in
2236
+ * DotnsRegistry.sol — it only ever returns a bool), so that branch
2237
+ * propagates as an unexpected failure rather than being folded into
2238
+ * "absent".
2239
+ */
2240
+ async probeIsAuthorised(node, account) {
2241
+ this.ensureConnected();
2242
+ if (!this.clientWrapper) throw new Error("probeIsAuthorised: polkadot-api client not available");
2243
+ const contractAddress = this._contracts.DOTNS_REGISTRY;
2244
+ const isAuthorisedAbi = DOTNS_REGISTRY_ABI;
2245
+ const isAuthorisedArgs = [node, account];
2246
+ const encodedCallData = encodeFunctionData({ abi: isAuthorisedAbi, functionName: "isAuthorised", args: isAuthorisedArgs });
2247
+ const callResult = await this.clientWrapper.performDryRunCall(this.substrateAddress, contractAddress, 0n, encodedCallData);
2248
+ if (callResult.result.isOk) {
2249
+ const rawData = callResult.result.value.data ?? "0x";
2250
+ if (rawData.length > 2) {
2251
+ return decodeFunctionResult({ abi: DOTNS_REGISTRY_ABI, functionName: "isAuthorised", data: rawData });
2252
+ }
2253
+ return null;
2254
+ }
2255
+ const errorData = callResult.result.value;
2256
+ const revertData = errorData?.data;
2257
+ const revertFlags = errorData?.flags;
2258
+ if (isBareRevertResult(revertData, revertFlags)) return null;
2259
+ throw new Error(formatContractDryRunFailure({
2260
+ revertData,
2261
+ revertFlags,
2262
+ gasConsumed: callResult.gasConsumed,
2263
+ gasRequired: callResult.gasRequired,
2264
+ storageDeposit: callResult.storageDeposit?.value
2265
+ }, {
2266
+ contractAddress,
2267
+ functionName: "isAuthorised",
2268
+ signerSubstrateAddress: this.substrateAddress,
2269
+ signerEvmAddress: this.evmAddress ?? void 0,
2270
+ value: 0n,
2271
+ encodedData: encodedCallData,
2272
+ args: [node, account],
2273
+ contracts: this._contracts
2274
+ }));
2275
+ }
2276
+ /**
2277
+ * Ask the registry who may write `node`, per DotnsRegistry.isAuthorised —
2278
+ * the contract's own "single source of truth" for owner-gated entry
2279
+ * points (setSubnodeOwner, setSubnodeResolver, setResolver): a non-zero
2280
+ * stored subnode owner must equal `account`; otherwise, for a tokenised
2281
+ * node, the registrar's ERC-721 owner, an operator-for-all delegate, or a
2282
+ * single-token approval all qualify. Strictly wider than a raw
2283
+ * owner()-equality test, which refuses an approved/operator delegate the
2284
+ * chain would accept — the contract's own comment calls operator-for-all
2285
+ * "the common marketplace / escrow delegation path".
2286
+ *
2287
+ * Falls back to owner()-equality when isAuthorised is absent from this
2288
+ * registry deployment (a bare revert on the probe) so a missing accessor
2289
+ * never turns into a refusal. `owner` is always the node's registry-read
2290
+ * owner (for informative messaging), independent of which path decided
2291
+ * `authorised`.
2292
+ */
2293
+ async checkNodeAuthorization(node, account) {
2294
+ this.ensureConnected();
2295
+ const owner = await withTimeout(
2296
+ this.contractCallNullable(this._contracts.DOTNS_REGISTRY, DOTNS_REGISTRY_ABI, "owner", [node]),
2297
+ 3e4,
2298
+ "owner"
2299
+ );
2300
+ const authorised = await this.probeIsAuthorised(node, account);
2301
+ if (authorised !== null) return { authorised, owner };
2302
+ return { authorised: owner !== null && owner.toLowerCase() === account.toLowerCase(), owner };
2303
+ }
2102
2304
  /** Live transfer-fee quote. transferFloor is a pure PopRules view — it
2103
2305
  * classifies the label and reads both tiers, so it works BEFORE the name is
2104
2306
  * registered (unlike quoteTransferFee, which reverts on an unregistered token). */
@@ -2149,9 +2351,12 @@ var DotNS = class _DotNS {
2149
2351
  * transferName/transferFrom — the registrar has no token for them. Instead
2150
2352
  * the *parent-domain* owner reassigns a subname via the registry's
2151
2353
  * setSubnodeOwner (the same call registerSubdomain uses to create it).
2152
- * Authorisation is on parent ownership, not on the subname's current owner,
2153
- * so the connected signer must own `${parentLabel}.${this._tld}`. Idempotent:
2154
- * a no-op when the recipient already owns it.
2354
+ * Authorisation is on the parent node, not on the subname's current owner
2355
+ * checked via the registry's own isAuthorised(parentNode, account)
2356
+ * (checkNodeAuthorization), so the connected signer may be the parent's
2357
+ * owner OR an operator-for-all/single-token approval on it, matching what
2358
+ * the chain itself allows on setSubnodeOwner. Idempotent: a no-op when the
2359
+ * recipient already owns it.
2155
2360
  *
2156
2361
  * Every node here is derived via computeSubnodeIds, which routes through the
2157
2362
  * single ensNode() primitive (#1304, see the comment above ensNode) — the
@@ -2167,17 +2372,13 @@ var DotNS = class _DotNS {
2167
2372
  const fullName = `${sublabel}.${parentLabel}.${this._tld}`;
2168
2373
  assertNotZeroRecipient(toH160, fullName);
2169
2374
  const { parentNode, subnode } = computeSubnodeIds(sublabel, parentLabel, this._tld);
2170
- const parentOwner = await withTimeout(
2171
- this.contractCallNullable(this._contracts.DOTNS_REGISTRY, DOTNS_REGISTRY_ABI, "owner", [parentNode]),
2172
- 3e4,
2173
- "owner"
2174
- );
2375
+ const { authorised: parentAuthorised, owner: parentOwner } = await this.checkNodeAuthorization(parentNode, this.evmAddress);
2175
2376
  if (!parentOwner || parentOwner === zeroAddress) {
2176
2377
  throw new Error(`Cannot transfer ${fullName}: parent ${parentLabel}.${this._tld} is not registered.`);
2177
2378
  }
2178
- if (parentOwner.toLowerCase() !== this.evmAddress.toLowerCase()) {
2379
+ if (!parentAuthorised) {
2179
2380
  throw new Error(
2180
- `Cannot transfer ${fullName}: it is a subname, which only the owner of the parent ${parentLabel}.${this._tld} can reassign (subnames are not transferable tokens). Parent is owned by ${parentOwner}, but the signer is ${this.evmAddress}. Sign as the parent owner (e.g. pass its --mnemonic).`
2381
+ `Cannot transfer ${fullName}: it is a subname, which only the owner of the parent ${parentLabel}.${this._tld} (or an account it has approved) can reassign (subnames are not transferable tokens). Parent is owned by ${parentOwner}, but the signer is ${this.evmAddress} and is not approved. Sign as the parent owner, or as an approved account (e.g. pass its --mnemonic).`
2181
2382
  );
2182
2383
  }
2183
2384
  const current = await withTimeout(
@@ -2190,12 +2391,13 @@ var DotNS = class _DotNS {
2190
2391
  return { status: "skipped-already-owned" };
2191
2392
  }
2192
2393
  const subnodeRecord = { parentNode, subLabel: sublabel, parentLabel, owner: toH160 };
2394
+ const { abi: setSubnodeOwnerAbi, args: setSubnodeOwnerArgs } = await this.buildSetSubnodeOwnerCall(subnodeRecord);
2193
2395
  const txRes = await this.contractTransaction(
2194
2396
  this._contracts.DOTNS_REGISTRY,
2195
2397
  0n,
2196
- DOTNS_REGISTRY_ABI,
2398
+ setSubnodeOwnerAbi,
2197
2399
  "setSubnodeOwner",
2198
- [subnodeRecord],
2400
+ setSubnodeOwnerArgs,
2199
2401
  statusCallback
2200
2402
  );
2201
2403
  const after = await withTimeout(
@@ -2274,9 +2476,10 @@ var DotNS = class _DotNS {
2274
2476
  await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
2275
2477
  }
2276
2478
  };
2479
+ const { abi: setSubnodeOwnerAbi, args: setSubnodeOwnerArgs } = await this.buildSetSubnodeOwnerCall(subnodeRecord);
2277
2480
  const txResolution = await this.submitBatchedContractCalls(
2278
2481
  [
2279
- { contractAddress: this._contracts.DOTNS_REGISTRY, abi: DOTNS_REGISTRY_ABI, functionName: "setSubnodeOwner", args: [subnodeRecord] },
2482
+ { contractAddress: this._contracts.DOTNS_REGISTRY, abi: setSubnodeOwnerAbi, functionName: "setSubnodeOwner", args: setSubnodeOwnerArgs },
2280
2483
  { contractAddress: this._contracts.DOTNS_REGISTRY, abi: DOTNS_REGISTRY_ABI, functionName: "setResolver", args: [subnodeNode, this._contracts.DOTNS_CONTENT_RESOLVER] }
2281
2484
  ],
2282
2485
  (s) => console.log(` ${s}`),
@@ -6,7 +6,7 @@ import {
6
6
  } from "./chunk-HDGD5FYX.js";
7
7
  import {
8
8
  VERSION
9
- } from "./chunk-3CES4RZG.js";
9
+ } from "./chunk-NXZHFDRN.js";
10
10
 
11
11
  // src/auth-config.ts
12
12
  import { existsSync, readdirSync } from "fs";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  package_default,
3
3
  writeRunState
4
- } from "./chunk-3TDAKT7Y.js";
4
+ } from "./chunk-S4ZROJ5B.js";
5
5
 
6
6
  // src/memory-report.ts
7
7
  import * as fs2 from "fs";