bulletin-deploy 0.18.4 → 0.19.0-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 (57) hide show
  1. package/README.md +1 -1
  2. package/dist/auth-config.js +4 -4
  3. package/dist/bug-report.js +4 -4
  4. package/dist/chain-health.js +3 -3
  5. package/dist/{chunk-JGIPHFBT.js → chunk-7G554R3V.js} +2 -2
  6. package/dist/{chunk-GZ4O5DIU.js → chunk-CQSTGES3.js} +1 -1
  7. package/dist/chunk-DMNLTQGA.js +64 -0
  8. package/dist/{chunk-CVQ7DJHG.js → chunk-I5PNPQZO.js} +64 -3
  9. package/dist/{chunk-7I3MTGOR.js → chunk-K5SP4OUC.js} +19 -1
  10. package/dist/{chunk-5HFXRNCQ.js → chunk-KFUL2F3N.js} +1 -1
  11. package/dist/{chunk-5MS64TTR.js → chunk-LE73Q6DA.js} +2 -2
  12. package/dist/{chunk-MJCXPKBL.js → chunk-NHH24DSU.js} +62 -14
  13. package/dist/{chunk-WEVSXHII.js → chunk-NKDGBZBP.js} +1 -1
  14. package/dist/{chunk-A55GRHCX.js → chunk-NVWPB3S3.js} +3 -3
  15. package/dist/{chunk-HDGD5FYX.js → chunk-NYNAUG4V.js} +13 -4
  16. package/dist/{chunk-WCGCM45C.js → chunk-OYQAO5ED.js} +11 -44
  17. package/dist/{chunk-TTUXQMD3.js → chunk-Q3DZJVLB.js} +1 -1
  18. package/dist/{chunk-STXAFUTQ.js → chunk-QJJJA4PQ.js} +4 -1
  19. package/dist/{chunk-E7PNP2NP.js → chunk-WN4Y7P24.js} +3 -3
  20. package/dist/{chunk-RXV677CJ.js → chunk-YTRNJMI4.js} +7 -36
  21. package/dist/{chunk-4NIOCONP.js → chunk-ZD43DXG6.js} +1 -1
  22. package/dist/{chunk-K23BCUAG.js → chunk-ZI5RS5PN.js} +21 -16
  23. package/dist/chunk-probe.js +3 -3
  24. package/dist/commands/login.js +13 -13
  25. package/dist/commands/logout.js +5 -5
  26. package/dist/commands/transfer.js +6 -6
  27. package/dist/commands/whoami.js +4 -4
  28. package/dist/deploy-actors.js +8 -8
  29. package/dist/deploy.d.ts +1 -1
  30. package/dist/deploy.js +13 -13
  31. package/dist/dotns-protocol.d.ts +32 -1
  32. package/dist/dotns-protocol.js +5 -1
  33. package/dist/dotns.d.ts +12 -3
  34. package/dist/dotns.js +8 -6
  35. package/dist/environments.d.ts +5 -2
  36. package/dist/environments.js +3 -1
  37. package/dist/index.d.ts +1 -1
  38. package/dist/index.js +21 -20
  39. package/dist/manifest/byte-budget.js +2 -1
  40. package/dist/manifest/compose.d.ts +20 -0
  41. package/dist/manifest/compose.js +8 -0
  42. package/dist/manifest/config-load.js +2 -2
  43. package/dist/manifest/publish.js +18 -17
  44. package/dist/manifest/schema.js +1 -1
  45. package/dist/manifest/types.d.ts +29 -1
  46. package/dist/memory-report.js +2 -2
  47. package/dist/merkle.js +13 -13
  48. package/dist/personhood/bootstrap.js +7 -7
  49. package/dist/personhood/people-client.js +6 -6
  50. package/dist/run-state.js +1 -1
  51. package/dist/sss-allowance-cache.js +5 -5
  52. package/dist/storage-signer.js +13 -13
  53. package/dist/telemetry.d.ts +1 -1
  54. package/dist/telemetry.js +2 -2
  55. package/dist/version-check.js +3 -3
  56. package/docs/product-manifest.md +76 -0
  57. package/package.json +3 -3
package/README.md CHANGED
@@ -71,7 +71,7 @@ Key options:
71
71
  | `--no-transfer-to-signedin-user` | When signed in, sign every DotNS tx with your mobile session instead of the default register-as-worker-then-hand-over flow. |
72
72
  | `--to <0xH160>` | Recipient address for the `transfer` subcommand. Defaults to the signed-in account. |
73
73
  | `--js-merkle` | Use pure-JS merkleization (no IPFS Kubo binary required). |
74
- | `--config <path>` | Explicit path to `bulletin-deploy.config.ts` for product deploys. |
74
+ | `--config <path>` | Explicit path to `bulletin-deploy.config.ts` for product deploys. See [`docs/product-manifest.md`](docs/product-manifest.md). |
75
75
  | `--tag "..."` | Label the deploy in telemetry. |
76
76
  | `--version` | Print the installed version and exit. |
77
77
 
@@ -9,12 +9,12 @@ import {
9
9
  getPeopleChainEndpoints,
10
10
  hasPersistedSession,
11
11
  resolveBulletinEndpoints
12
- } from "./chunk-5MS64TTR.js";
12
+ } from "./chunk-LE73Q6DA.js";
13
13
  import "./chunk-TSPERKUS.js";
14
- import "./chunk-HDGD5FYX.js";
14
+ import "./chunk-NYNAUG4V.js";
15
15
  import "./chunk-ZOC4GITL.js";
16
- import "./chunk-STXAFUTQ.js";
17
- import "./chunk-E7PNP2NP.js";
16
+ import "./chunk-QJJJA4PQ.js";
17
+ import "./chunk-WN4Y7P24.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-A55GRHCX.js";
14
- import "./chunk-TTUXQMD3.js";
15
- import "./chunk-STXAFUTQ.js";
16
- import "./chunk-E7PNP2NP.js";
13
+ } from "./chunk-NVWPB3S3.js";
14
+ import "./chunk-Q3DZJVLB.js";
15
+ import "./chunk-QJJJA4PQ.js";
16
+ import "./chunk-WN4Y7P24.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-5HFXRNCQ.js";
7
- import "./chunk-STXAFUTQ.js";
8
- import "./chunk-E7PNP2NP.js";
6
+ } from "./chunk-KFUL2F3N.js";
7
+ import "./chunk-QJJJA4PQ.js";
8
+ import "./chunk-WN4Y7P24.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-MJCXPKBL.js";
7
+ } from "./chunk-NHH24DSU.js";
8
8
  import {
9
9
  DOT_PRODUCT_ID
10
- } from "./chunk-5MS64TTR.js";
10
+ } from "./chunk-LE73Q6DA.js";
11
11
 
12
12
  // src/deploy-actors.ts
13
13
  var DEFAULT_WORKER_SURI = DEFAULT_MNEMONIC;
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-GRPLHUYC.js";
4
4
  import {
5
5
  DOT_DAPP_ID
6
- } from "./chunk-5MS64TTR.js";
6
+ } from "./chunk-LE73Q6DA.js";
7
7
 
8
8
  // src/sss-allowance-cache.ts
9
9
  import { mkdir, readFile, writeFile, unlink } from "fs/promises";
@@ -0,0 +1,64 @@
1
+ // src/manifest/compose.ts
2
+ function composeRoot(config, iconCid) {
3
+ const labels = config.domain.split(".");
4
+ const selfLabel = (labels[labels.length - 2] ?? labels[0]).toLowerCase();
5
+ const trustedProducts = normalizeTrustedProducts(
6
+ config.trustedProducts,
7
+ selfLabel
8
+ );
9
+ return {
10
+ $v: 1,
11
+ displayName: config.displayName,
12
+ description: config.description,
13
+ icon: { cid: iconCid, format: config.icon.format },
14
+ ...trustedProducts !== void 0 ? { trustedProducts } : {}
15
+ };
16
+ }
17
+ function normalizeTrustedProducts(input, selfLabel) {
18
+ if (input === void 0) return void 0;
19
+ const ids = Object.keys(input).filter(
20
+ (id) => id.toLowerCase() !== selfLabel && input[id].length > 0
21
+ ).sort();
22
+ if (ids.length === 0) return void 0;
23
+ return Object.fromEntries(
24
+ ids.map((id) => [id, normalizeGrants(input[id])])
25
+ );
26
+ }
27
+ function normalizeGrants(grants) {
28
+ const unique = [...new Set(grants)];
29
+ return unique.includes("all") ? ["all"] : unique.sort();
30
+ }
31
+ function composeExecutable(exec) {
32
+ if (exec.kind === "app") {
33
+ return "manifest" in exec ? exec.manifest : { $v: 1, kind: "app", appVersion: exec.appVersion };
34
+ }
35
+ if (exec.kind === "widget") {
36
+ return {
37
+ $v: 1,
38
+ kind: "widget",
39
+ appVersion: exec.appVersion,
40
+ dimensions: exec.dimensions,
41
+ ...exec.description !== void 0 ? { description: exec.description } : {}
42
+ };
43
+ }
44
+ if (exec.kind === "funding") {
45
+ return {
46
+ $v: 1,
47
+ kind: "funding",
48
+ appVersion: exec.appVersion,
49
+ modes: exec.modes
50
+ };
51
+ }
52
+ return {
53
+ $v: 1,
54
+ kind: "worker",
55
+ appVersion: exec.appVersion,
56
+ entrypoint: exec.entrypoint,
57
+ includes: exec.includes
58
+ };
59
+ }
60
+
61
+ export {
62
+ composeRoot,
63
+ composeExecutable
64
+ };
@@ -11,15 +11,53 @@ var EXECUTABLE_KINDS = [
11
11
  KIND_WORKER
12
12
  ];
13
13
  var FUNDING_MODES = ["CARD", "BANK", "CRYPTO"];
14
+ var GRANTED_VALUES = ["all", "storage", "context"];
14
15
  var LABEL = String.raw`(?!-)[a-z0-9-]{1,63}(?<!-)`;
15
16
  var TLD_FRAGMENT = "[a-z]{2,63}";
16
17
  var DOMAIN_RE = new RegExp(`^${LABEL}(\\.${LABEL})*\\.${TLD_FRAGMENT}$`, "i");
18
+ var PRODUCT_LABEL_RE = new RegExp(`^${LABEL}$`);
17
19
  function isPlainObject(value) {
18
20
  return typeof value === "object" && value !== null && !Array.isArray(value);
19
21
  }
20
22
  function isNonEmptyString(value) {
21
23
  return typeof value === "string" && value.length > 0;
22
24
  }
25
+ function validateTrustedProducts(value, label, strict) {
26
+ if (!isPlainObject(value))
27
+ return [`${label} trustedProducts must be an object`];
28
+ const errors = [];
29
+ for (const [key, grants] of Object.entries(value)) {
30
+ if (key.length === 0) {
31
+ errors.push(`${label} trustedProducts key must not be empty`);
32
+ } else if (key.includes(".")) {
33
+ if (strict) {
34
+ errors.push(
35
+ `${label} trustedProducts key '${key}' must not include a TLD suffix (the host appends its own TLD) \u2014 use '${key.split(".")[0]}' instead of '${key}'`
36
+ );
37
+ }
38
+ } else if (key !== key.toLowerCase()) {
39
+ errors.push(`${label} trustedProducts key '${key}' must be lowercase`);
40
+ } else if (!PRODUCT_LABEL_RE.test(key)) {
41
+ errors.push(
42
+ `${label} trustedProducts key '${key}' must be a valid dotNS label`
43
+ );
44
+ }
45
+ if (!Array.isArray(grants)) {
46
+ errors.push(`${label} trustedProducts['${key}'] must be an array`);
47
+ continue;
48
+ }
49
+ if (strict) {
50
+ errors.push(
51
+ ...rejectUnknownEntries(
52
+ grants,
53
+ GRANTED_VALUES,
54
+ `${label} trustedProducts['${key}']`
55
+ )
56
+ );
57
+ }
58
+ }
59
+ return errors;
60
+ }
23
61
  function isAppVersion(value) {
24
62
  if (!Array.isArray(value)) return false;
25
63
  if (value.length !== 3 && value.length !== 4) return false;
@@ -32,6 +70,11 @@ function isAppVersion(value) {
32
70
  function rejectUnknownFields(input, allowed, prefix) {
33
71
  return Object.keys(input).filter((key) => !allowed.includes(key)).map((key) => `${prefix}contains unknown field '${key}'`);
34
72
  }
73
+ function rejectUnknownEntries(values, allowed, describe) {
74
+ return values.filter((value) => !allowed.includes(value)).map(
75
+ (value) => `${describe} entries must be one of ${allowed.join(", ")} (got ${JSON.stringify(value)})`
76
+ );
77
+ }
35
78
  function validateRelativeEntrypoint(value, suffix, prefix) {
36
79
  if (!isNonEmptyString(value))
37
80
  return [`${prefix}entrypoint must be a non-empty string`];
@@ -246,9 +289,7 @@ function validateFundingFields(input, p) {
246
289
  if (!Array.isArray(input.modes) || input.modes.length === 0) {
247
290
  return [`${p}modes must be a non-empty array`];
248
291
  }
249
- return input.modes.filter((mode) => !FUNDING_MODES.includes(mode)).map(
250
- (mode) => `${p}modes entries must be one of ${FUNDING_MODES.join(", ")} (got ${JSON.stringify(mode)})`
251
- );
292
+ return rejectUnknownEntries(input.modes, FUNDING_MODES, `${p}modes`);
252
293
  }
253
294
  function validateWorkerFields(input, p) {
254
295
  const errors = [];
@@ -294,6 +335,16 @@ function validateRootManifest(input) {
294
335
  );
295
336
  }
296
337
  }
338
+ if (input.trustedProducts !== void 0) {
339
+ errors.push(
340
+ ...validateTrustedProducts(
341
+ input.trustedProducts,
342
+ "root manifest",
343
+ /* strict */
344
+ false
345
+ )
346
+ );
347
+ }
297
348
  return errors.length === 0 ? { ok: true, value: input } : { ok: false, errors };
298
349
  }
299
350
  function validateExecutableManifest(input) {
@@ -363,6 +414,16 @@ function validateProductConfig(input) {
363
414
  );
364
415
  }
365
416
  }
417
+ if (input.trustedProducts !== void 0) {
418
+ errors.push(
419
+ ...validateTrustedProducts(
420
+ input.trustedProducts,
421
+ "product config",
422
+ /* strict */
423
+ true
424
+ )
425
+ );
426
+ }
366
427
  if (!Array.isArray(input.executables) || input.executables.length === 0) {
367
428
  errors.push("product config executables must be a non-empty array");
368
429
  } else {
@@ -16,12 +16,28 @@ var DOTNS_ABI_PROFILES = {
16
16
  discriminator: "isPopIssued"
17
17
  }
18
18
  };
19
+ var HIGHEST_VERIFIED_DOTNS_RELEASE = "0.8.0";
20
+ function classifyDeclaredProtocolVersion(raw) {
21
+ if (!raw) return null;
22
+ const parts = raw.split("-")[0].split(".").slice(0, 3);
23
+ if (parts.length < 2 || !parts.every((part) => /^\d+$/.test(part))) return null;
24
+ return {
25
+ raw,
26
+ profile: "v0.6.0",
27
+ // Numeric collation, so 0.10.0 sorts above 0.8.0 rather than below it.
28
+ aboveVerifiedCeiling: parts.join(".").localeCompare(HIGHEST_VERIFIED_DOTNS_RELEASE, void 0, { numeric: true }) > 0
29
+ };
30
+ }
19
31
  function classifyProtocolVersion(probe) {
20
32
  const { hasCode, pricingVersionOk, startingPriceOk, isPopIssuedOk } = probe;
21
33
  if (hasCode === false) {
22
34
  return {
23
35
  profile: null,
24
- reason: "No contract deployed at this address \u2014 could not detect the DotNS ABI profile because no contract code was found here. Check environments.json / --contract config for this network."
36
+ reason: (
37
+ // No config pointer here: this function has no environment context, so it
38
+ // cannot know which file holds the address. The caller appends that.
39
+ "No contract deployed at this address: could not detect the DotNS ABI profile because no contract code was found here."
40
+ )
25
41
  };
26
42
  }
27
43
  if (pricingVersionOk) return { profile: isPopIssuedOk === true ? "v0.6.0" : "v0.5.8-rc1" };
@@ -143,6 +159,8 @@ function getAdapter(profile) {
143
159
 
144
160
  export {
145
161
  DOTNS_ABI_PROFILES,
162
+ HIGHEST_VERIFIED_DOTNS_RELEASE,
163
+ classifyDeclaredProtocolVersion,
146
164
  classifyProtocolVersion,
147
165
  OLD_CONTROLLER_ABI,
148
166
  V0_5_8_RC1_CONTROLLER_ABI,
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  emitIntervalSpan,
3
3
  setDeployAttribute
4
- } from "./chunk-STXAFUTQ.js";
4
+ } from "./chunk-QJJJA4PQ.js";
5
5
 
6
6
  // src/chain-health.ts
7
7
  var DEFAULT_WINDOW_BLOCKS = 10;
@@ -3,10 +3,10 @@ import {
3
3
  } from "./chunk-TSPERKUS.js";
4
4
  import {
5
5
  loadEnvironments
6
- } from "./chunk-HDGD5FYX.js";
6
+ } from "./chunk-NYNAUG4V.js";
7
7
  import {
8
8
  VERSION
9
- } from "./chunk-STXAFUTQ.js";
9
+ } from "./chunk-QJJJA4PQ.js";
10
10
 
11
11
  // src/auth-config.ts
12
12
  import { existsSync, readdirSync } from "fs";
@@ -7,13 +7,13 @@ import {
7
7
  } from "./chunk-AYAVRPAP.js";
8
8
  import {
9
9
  validateContractAddresses
10
- } from "./chunk-HDGD5FYX.js";
10
+ } from "./chunk-NYNAUG4V.js";
11
11
  import {
12
12
  NonRetryableError
13
13
  } from "./chunk-ZOC4GITL.js";
14
14
  import {
15
15
  ChainHealthSampler
16
- } from "./chunk-5HFXRNCQ.js";
16
+ } from "./chunk-KFUL2F3N.js";
17
17
  import {
18
18
  captureWarning,
19
19
  markCodePath,
@@ -21,14 +21,16 @@ import {
21
21
  setDeploySentryTag,
22
22
  truncateAddress,
23
23
  withSpan
24
- } from "./chunk-STXAFUTQ.js";
24
+ } from "./chunk-QJJJA4PQ.js";
25
25
  import {
26
+ HIGHEST_VERIFIED_DOTNS_RELEASE,
26
27
  POP_CONTROLLER_PROBE_ABI,
27
28
  PROTOCOL_PROBE_LABEL,
29
+ classifyDeclaredProtocolVersion,
28
30
  classifyProtocolVersion,
29
31
  getAdapter,
30
32
  withTenPercentBuffer
31
- } from "./chunk-7I3MTGOR.js";
33
+ } from "./chunk-K5SP4OUC.js";
32
34
 
33
35
  // src/dotns.ts
34
36
  import crypto from "crypto";
@@ -370,7 +372,8 @@ var DOTNS_REGISTRAR_ABI = [
370
372
  ];
371
373
  var DOTNS_REGISTRAR_TRANSFER_ABI = [
372
374
  ...DOTNS_REGISTRAR_ABI,
373
- { inputs: [{ name: "from", type: "address" }, { name: "to", type: "address" }, { name: "tokenId", type: "uint256" }], name: "transferFrom", outputs: [], stateMutability: "payable", type: "function" }
375
+ { inputs: [{ name: "from", type: "address" }, { name: "to", type: "address" }, { name: "tokenId", type: "uint256" }], name: "transferFrom", outputs: [], stateMutability: "payable", type: "function" },
376
+ { inputs: [{ name: "tokenId", type: "uint256" }], name: "isSoulbound", outputs: [{ name: "", type: "bool" }], stateMutability: "view", type: "function" }
374
377
  ];
375
378
  var POP_RULES_ABI = [
376
379
  { inputs: [{ name: "name", type: "string" }], name: "classifyName", outputs: [{ name: "requirement", type: "uint8" }, { name: "message", type: "string" }], stateMutability: "pure", type: "function" },
@@ -437,7 +440,9 @@ var DOTNS_CONTENT_RESOLVER_ABI = [
437
440
  ];
438
441
  var DOTNS_PROTOCOL_REGISTRY_ABI = [
439
442
  { inputs: [], name: "tld", outputs: [{ name: "", type: "string" }], stateMutability: "view", type: "function" },
440
- { inputs: [], name: "tldNode", outputs: [{ name: "", type: "bytes32" }], stateMutability: "view", type: "function" }
443
+ { inputs: [], name: "tldNode", outputs: [{ name: "", type: "bytes32" }], stateMutability: "view", type: "function" },
444
+ // v0.8.0 and up; reverts below it, empty until a release is declared.
445
+ { inputs: [], name: "protocolVersion", outputs: [{ name: "", type: "string" }], stateMutability: "view", type: "function" }
441
446
  ];
442
447
  var DOTNS_TEXT_RESOLVER_ABI = [
443
448
  { inputs: [{ name: "node", type: "bytes32" }, { name: "key", type: "string" }, { name: "value", type: "string" }], name: "setText", outputs: [], stateMutability: "nonpayable", type: "function" },
@@ -822,6 +827,9 @@ function isShortNameBand(baseLength) {
822
827
  function shortNamesClosedReason(label, baseLength, tld, environmentId) {
823
828
  return `${label}.${tld} has a ${baseLength}-character base and short names are not on sale on ${environmentId ?? "this environment"}. PopRules.shortNamesEnabled is off, which closes the 6 to 8 character band to every signer, whatever their personhood status. Use a base of 9 characters or more, for example ${exampleNoStatusLabel(label, tld)}.`;
824
829
  }
830
+ function soulboundTransferReason(label, tld) {
831
+ return `${label}.${tld} is soulbound and cannot be transferred. It was issued through the PoP gateway, which binds a name to the account that received it. Nothing clears the flag, so no account can move this name at any point in the future.`;
832
+ }
825
833
  function parseDomainName(input, tld = DEFAULT_TLD) {
826
834
  const configuredSuffix = `.${tld}`;
827
835
  let name;
@@ -1324,6 +1332,7 @@ var DotNS = class _DotNS {
1324
1332
  _isPhoneSigner = false;
1325
1333
  _localMnemonic = null;
1326
1334
  _contracts = CONTRACTS;
1335
+ _contractSources = {};
1327
1336
  _nativeToEthRatio = NATIVE_TO_ETH_RATIO;
1328
1337
  _environmentId = null;
1329
1338
  // #1095: the resolved env's declared `network`, when connect() was given
@@ -1509,9 +1518,10 @@ var DotNS = class _DotNS {
1509
1518
  this.assetHubEndpoints = options.assetHubEndpoints;
1510
1519
  }
1511
1520
  if (options.contracts && Object.keys(options.contracts).length > 0) {
1512
- validateContractAddresses(options.contracts, options.environmentId ?? "unknown");
1521
+ validateContractAddresses(options.contracts, options.environmentId ?? "unknown", options.contractSources);
1513
1522
  this._contracts = { ...CONTRACTS, ...options.contracts };
1514
1523
  }
1524
+ this._contractSources = options.contractSources ?? {};
1515
1525
  if (options.environmentId) {
1516
1526
  this._environmentId = options.environmentId;
1517
1527
  }
@@ -1901,7 +1911,8 @@ var DotNS = class _DotNS {
1901
1911
  });
1902
1912
  }
1903
1913
  /**
1904
- * Low-level dry-run read against DotnsProtocolRegistry (tld() / tldNode()).
1914
+ * Low-level dry-run read against DotnsProtocolRegistry (tld() / tldNode() /
1915
+ * protocolVersion()).
1905
1916
  * Deliberately does NOT reuse contractCall/contractCallNullable: both of
1906
1917
  * those throw on a revert or on empty data, which would make "contract
1907
1918
  * doesn't support this function yet" indistinguishable from "the RPC call
@@ -2000,25 +2011,43 @@ var DotNS = class _DotNS {
2000
2011
  const popRulesAddress = this._contracts.POP_RULES;
2001
2012
  const popControllerAddress = this._contracts.DOTNS_POP_CONTROLLER;
2002
2013
  const env = this._environmentId ?? "unknown";
2003
- const [hasCodeResult, pricingVersionOk, startingPriceOk, isPopIssuedOk] = await Promise.all([
2014
+ const [hasCodeResult, pricingVersionOk, startingPriceOk, isPopIssuedOk, declaredResult] = await Promise.all([
2004
2015
  this.clientWrapper.hasContractCode(popRulesAddress),
2005
2016
  this.probeViewFunctionOk(popRulesAddress, getAdapter("v0.5.8-rc1").popRulesAbi, "pricingVersion", []),
2006
2017
  this.probeViewFunctionOk(popRulesAddress, getAdapter("poprules-startingPrice").popRulesAbi, "startingPrice", []),
2007
- popControllerAddress ? this.probeViewFunctionOk(popControllerAddress, POP_CONTROLLER_PROBE_ABI, "isPopIssued", [PROTOCOL_PROBE_LABEL]) : Promise.resolve(null)
2018
+ popControllerAddress ? this.probeViewFunctionOk(popControllerAddress, POP_CONTROLLER_PROBE_ABI, "isPopIssued", [PROTOCOL_PROBE_LABEL]) : Promise.resolve(null),
2019
+ // Informational, so an RPC failure must not fail a connect the probes
2020
+ // can classify. Caught here, not in dryRunRegistryString, which
2021
+ // propagates on purpose for tld() — swallowing there picks a wrong TLD.
2022
+ this.dryRunRegistryString("protocolVersion").catch(() => ({ ok: false }))
2008
2023
  ]);
2009
2024
  const classification = classifyProtocolVersion({ hasCode: hasCodeResult, pricingVersionOk, startingPriceOk, isPopIssuedOk });
2010
2025
  if (classification.profile === null) {
2011
- throw new Error(`${env} (${dotnsContractName(popRulesAddress, this._contracts)} ${popRulesAddress}): ${classification.reason}`);
2026
+ throw new Error(`${env} (${dotnsContractName(popRulesAddress, this._contracts)} ${popRulesAddress}): ${classification.reason}${hasCodeResult === true ? "" : this.contractSourceHint("POP_RULES")}`);
2012
2027
  }
2013
2028
  const profile = classification.profile;
2029
+ const declared = classifyDeclaredProtocolVersion(declaredResult.ok ? declaredResult.value : null);
2014
2030
  this._protocolVersion = profile;
2015
2031
  this._adapter = getAdapter(profile);
2016
2032
  setDeployAttribute("deploy.dotns.protocol_version", profile);
2017
- console.log(` DotNS ABI profile ${profile} detected on ${env}`);
2033
+ setDeployAttribute("deploy.dotns.declared_release", declaredResult.ok ? declaredResult.value || "undeclared" : "unavailable");
2034
+ console.log(` DotNS ABI profile ${profile} detected on ${env}${declared ? ` (declares DotNS ${declared.raw})` : ""}`);
2035
+ if (declared && declared.profile !== profile) {
2036
+ console.log(` NOTE: ${env} declares DotNS ${declared.raw}, which is the ${declared.profile} profile, but the probes detect ${profile}. Using ${profile}: the probes read the ABI that is actually deployed.`);
2037
+ }
2038
+ if (declared?.aboveVerifiedCeiling) {
2039
+ console.log(` WARNING: ${env} declares DotNS ${declared.raw}, newer than ${HIGHEST_VERIFIED_DOTNS_RELEASE} \u2014 the newest release whose ABI has been checked against this client. Continuing on the ${profile} profile, which is correct only if that release changed nothing bulletin-deploy calls. Re-run the upstream ABI diff.`);
2040
+ }
2018
2041
  if (profile === "v0.5.8-rc1" && pricingVersionOk && isPopIssuedOk === null) {
2019
2042
  console.log(` NOTE: could not confirm whether ${env} is v0.5.8-rc1 or v0.6.0 \u2014 DOTNS_POP_CONTROLLER has no configured address here, so the isPopIssued discriminator probe was never attempted. Defaulting to v0.5.8-rc1 label semantics (registration itself is unaffected: both profiles share an identical ABI/adapter).`);
2020
2043
  }
2021
2044
  }
2045
+ // Ends an address error with where that address came from. A key with no
2046
+ // recorded source fell through to the defaults compiled into this module.
2047
+ contractSourceHint(name) {
2048
+ const src = this._contractSources[name];
2049
+ return src ? ` This address came from ${src}.` : ` No environment or --contract supplied ${name}, so this is the built-in default address.`;
2050
+ }
2022
2051
  async contractCall(contractAddress, contractAbi, functionName, args = []) {
2023
2052
  this.ensureConnected();
2024
2053
  if (!this.clientWrapper) throw new Error("contractCall: polkadot-api client not available");
@@ -2050,12 +2079,12 @@ var DotNS = class _DotNS {
2050
2079
  const env = this._environmentId ?? "(unset)";
2051
2080
  if (hasCode === false) {
2052
2081
  throw new Error(
2053
- `No contract deployed at ${contractAddress} (${name}) env=${env} \u2014 the dry-run call to ${functionName} returned empty success data, which on pallet-revive means the target address has no contract code. Check environments.json / --contract config for this network.`
2082
+ `No contract deployed at ${contractAddress} (${name}) env=${env} \u2014 the dry-run call to ${functionName} returned empty success data, which on pallet-revive means the target address has no contract code.${this.contractSourceHint(name)}`
2054
2083
  );
2055
2084
  }
2056
2085
  if (hasCode === null) {
2057
2086
  throw new Error(
2058
- `Contract call returned empty data \u2014 contract=${name} (${contractAddress}) env=${env} functionName=${functionName}. Could not verify whether contract code exists at this address (runtime code-presence query failed); investigate the contract/ABI or the configured address.`
2087
+ `Contract call returned empty data \u2014 contract=${name} (${contractAddress}) env=${env} functionName=${functionName}. Could not verify whether contract code exists at this address (runtime code-presence query failed); investigate the contract/ABI or the configured address.${this.contractSourceHint(name)}`
2059
2088
  );
2060
2089
  }
2061
2090
  throw new Error(
@@ -2350,6 +2379,21 @@ var DotNS = class _DotNS {
2350
2379
  if (authorised !== null) return { authorised, owner };
2351
2380
  return { authorised: owner !== null && owner.toLowerCase() === account.toLowerCase(), owner };
2352
2381
  }
2382
+ /** False when the registrar gave no usable answer, including one too old to
2383
+ * have the function at all: let the chain refuse rather than guess. A read
2384
+ * that never completed propagates, so a network blip is not "not soulbound". */
2385
+ async readIsSoulbound(tokenId) {
2386
+ try {
2387
+ return await withTimeout(
2388
+ this.contractCall(this._contracts.DOTNS_REGISTRAR, DOTNS_REGISTRAR_TRANSFER_ABI, "isSoulbound", [tokenId]),
2389
+ 3e4,
2390
+ "isSoulbound"
2391
+ ) === true;
2392
+ } catch (e) {
2393
+ if (READ_NEVER_COMPLETED_RE.test(e?.message ?? String(e))) throw e;
2394
+ return false;
2395
+ }
2396
+ }
2353
2397
  /** Live transfer-fee quote. transferFloor is a pure PopRules view — it
2354
2398
  * classifies the label and reads both tiers, so it works BEFORE the name is
2355
2399
  * registered (unlike quoteTransferFee, which reverts on an unregistered token). */
@@ -2379,6 +2423,9 @@ var DotNS = class _DotNS {
2379
2423
  if (owner.toLowerCase() !== this.evmAddress.toLowerCase()) {
2380
2424
  throw new Error(`Cannot transfer ${validated}.${this._tld}: it is owned by ${owner}, not the worker ${this.evmAddress}.`);
2381
2425
  }
2426
+ if (await this.readIsSoulbound(tokenId)) {
2427
+ throw new NonRetryableError(soulboundTransferReason(validated, this._tld));
2428
+ }
2382
2429
  const { feeWei, feeNative } = await this.quoteTransferFloorNative(validated, this.evmAddress, toH160);
2383
2430
  const txRes = await this.contractTransaction(
2384
2431
  this._contracts.DOTNS_REGISTRAR,
@@ -3808,6 +3855,7 @@ export {
3808
3855
  classifyDotnsLabel,
3809
3856
  canRegister,
3810
3857
  shortNamesClosedReason,
3858
+ soulboundTransferReason,
3811
3859
  parseDomainName,
3812
3860
  parseProofOfPersonhoodStatus,
3813
3861
  popStatusName,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  validateProductConfig
3
- } from "./chunk-CVQ7DJHG.js";
3
+ } from "./chunk-I5PNPQZO.js";
4
4
  import {
5
5
  NonRetryableError
6
6
  } from "./chunk-ZOC4GITL.js";
@@ -2,15 +2,15 @@ import {
2
2
  classifyErrorArea,
3
3
  isInteractive,
4
4
  promptYesNo
5
- } from "./chunk-TTUXQMD3.js";
5
+ } from "./chunk-Q3DZJVLB.js";
6
6
  import {
7
7
  VERSION,
8
8
  getCurrentSentryTraceId,
9
9
  resolveIssueRepoSlug
10
- } from "./chunk-STXAFUTQ.js";
10
+ } from "./chunk-QJJJA4PQ.js";
11
11
  import {
12
12
  package_default
13
- } from "./chunk-E7PNP2NP.js";
13
+ } from "./chunk-WN4Y7P24.js";
14
14
 
15
15
  // src/bug-report.ts
16
16
  import { execSync, execFileSync } from "child_process";
@@ -509,15 +509,23 @@ var ZERO_ADDRESS = "0x" + "0".repeat(40);
509
509
  function isValidContractAddress(addr) {
510
510
  return typeof addr === "string" && EVM_ADDRESS_RE.test(addr) && addr.toLowerCase() !== ZERO_ADDRESS;
511
511
  }
512
- function validateContractAddresses(contracts, envId) {
512
+ function validateContractAddresses(contracts, envId, sources) {
513
513
  for (const [name, addr] of Object.entries(contracts)) {
514
514
  if (!isValidContractAddress(addr)) {
515
515
  throw new Error(
516
- `Invalid contract address for ${name} in environment ${envId}: ${addr}. Check assets/environments.json against https://github.com/paritytech/dotns#deployments`
516
+ `Invalid contract address for ${name} in environment ${envId}: ${addr}. ` + (sources?.[name] ? `This address came from ${sources[name]}.` : `Check assets/environments.json against https://github.com/paritytech/dotns#deployments`)
517
517
  );
518
518
  }
519
519
  }
520
520
  }
521
+ function describeContractSources(envContracts, cliContracts, source, userFilePath, envId) {
522
+ const env = envId ?? "unknown";
523
+ const docOrigin = source === "file" && userFilePath ? `${userFilePath} (environment ${env})` : source === "hardcoded-fallback" ? `the built-in fallback for ${env}` : `assets/environments.json shipped with bulletin-deploy (environment ${env})`;
524
+ const out = {};
525
+ for (const key of Object.keys(envContracts)) out[key] = docOrigin;
526
+ for (const [key, addr] of Object.entries(cliContracts ?? {})) out[key] = `the --contract flag or contracts option (${key}=${addr})`;
527
+ return out;
528
+ }
521
529
  function isValidDoc(value) {
522
530
  if (!value || typeof value !== "object") return false;
523
531
  const v = value;
@@ -594,13 +602,13 @@ async function loadEnvironments(opts = {}) {
594
602
  for (const env of merged.environments) {
595
603
  if (env.contracts && Object.keys(env.contracts).length > 0) {
596
604
  try {
597
- validateContractAddresses(env.contracts, env.id);
605
+ validateContractAddresses(env.contracts, env.id, describeContractSources(env.contracts, void 0, "file", userFilePath, env.id));
598
606
  } catch (err) {
599
607
  warn(`bulletin-deploy: Warning: ${err?.message ?? err}`);
600
608
  }
601
609
  }
602
610
  }
603
- return { doc: merged, source: "file" };
611
+ return { doc: merged, source: "file", userFilePath };
604
612
  }
605
613
  const bundled = await readBundled(bundledPath, capture);
606
614
  if (bundled) return { doc: bundled, source: "bundled" };
@@ -722,6 +730,7 @@ export {
722
730
  defaultBundledPath,
723
731
  isValidContractAddress,
724
732
  validateContractAddresses,
733
+ describeContractSources,
725
734
  deepMergeEnvironments,
726
735
  loadEnvironments,
727
736
  resolveEndpoints,