bulletin-deploy 0.15.2 → 0.16.0-rc.2

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 (45) hide show
  1. package/assets/environments.json +1 -1
  2. package/bin/bulletin-deploy +20 -6
  3. package/dist/auth-config.js +4 -4
  4. package/dist/bug-report.js +4 -4
  5. package/dist/chain-health.js +3 -3
  6. package/dist/{chunk-DN4FDSVM.js → chunk-2BKPLQY2.js} +1 -1
  7. package/dist/{chunk-CD2NN5LL.js → chunk-33SYDRVZ.js} +1 -1
  8. package/dist/{chunk-SJPSTDCN.js → chunk-466FDICA.js} +1 -1
  9. package/dist/{chunk-KYBCFN3P.js → chunk-6C3JLASN.js} +2 -2
  10. package/dist/{chunk-CL7GQYYK.js → chunk-7CHUI4JX.js} +1 -1
  11. package/dist/{chunk-X7QKYH3U.js → chunk-BBPA3Q5K.js} +1 -1
  12. package/dist/{chunk-YXMWCUXP.js → chunk-D6AYE6KA.js} +1 -1
  13. package/dist/{chunk-JZNSDAZM.js → chunk-OORLC7SM.js} +9 -9
  14. package/dist/{chunk-EKBGOA24.js → chunk-OWCCOKOP.js} +2 -2
  15. package/dist/{chunk-CRNBI46S.js → chunk-PK3T6C6T.js} +3 -3
  16. package/dist/{chunk-4T6LXSOJ.js → chunk-SWD774NU.js} +1 -1
  17. package/dist/{chunk-ZPQO4PY2.js → chunk-SY2KD7SN.js} +123 -15
  18. package/dist/{chunk-2M65ETRD.js → chunk-VSLW6WXT.js} +1 -1
  19. package/dist/{chunk-SNIF5BSD.js → chunk-VTOT4GBT.js} +4 -4
  20. package/dist/{chunk-5HYWJO57.js → chunk-XFXTMHZX.js} +2 -2
  21. package/dist/chunk-probe.js +3 -3
  22. package/dist/commands/login.js +12 -12
  23. package/dist/commands/logout.js +5 -5
  24. package/dist/commands/transfer.js +20 -12
  25. package/dist/commands/whoami.js +4 -4
  26. package/dist/deploy-actors.js +7 -7
  27. package/dist/deploy.d.ts +11 -0
  28. package/dist/deploy.js +12 -12
  29. package/dist/dotns.d.ts +79 -10
  30. package/dist/dotns.js +15 -5
  31. package/dist/environments.js +1 -1
  32. package/dist/index.js +15 -15
  33. package/dist/manifest/config-load.js +2 -2
  34. package/dist/manifest/publish.js +13 -13
  35. package/dist/manifest/schema.js +1 -1
  36. package/dist/memory-report.js +2 -2
  37. package/dist/merkle.js +12 -12
  38. package/dist/personhood/bootstrap.js +5 -5
  39. package/dist/personhood/people-client.js +5 -5
  40. package/dist/run-state.js +1 -1
  41. package/dist/sss-allowance-cache.js +5 -5
  42. package/dist/storage-signer.js +12 -12
  43. package/dist/telemetry.js +2 -2
  44. package/dist/version-check.js +3 -3
  45. package/package.json +2 -2
@@ -11,7 +11,7 @@
11
11
  "backend": "https://polkadot-app-stg.parity.io/",
12
12
  "ipfs": "https://previewnet.substrate.dev",
13
13
  "webGateway": "testnet.li",
14
- "tld": "test",
14
+ "tld": "testnet",
15
15
  "autoAccountMapping": true,
16
16
  "nativeToEthRatio": 100000000,
17
17
  "registerStorageDeposit": 2000000000000,
@@ -259,7 +259,8 @@ Usage:
259
259
  bulletin-deploy login Sign in with your Polkadot mobile app
260
260
  bulletin-deploy logout Sign out and clear the session
261
261
  bulletin-deploy whoami Show the currently signed-in identity
262
- bulletin-deploy transfer <domain.dot> Hand a name you registered to the signed-in account (or --to)
262
+ bulletin-deploy transfer <domain.dot> Hand a name you registered to the signed-in account (or --to).
263
+ Also reassigns a subname (e.g. app.foo.dot) if you own its parent.
263
264
 
264
265
  Options:
265
266
  --env <id> Target environment from environments.json (default: paseo-next-v2).
@@ -312,7 +313,7 @@ Options:
312
313
  config at all. Mutually exclusive with --publish.
313
314
  --publish After deploy, list <domain> in the on-chain Publisher
314
315
  registry (Publisher.publish). Only takes effect on envs
315
- with a deployed Publisher (currently: paseo-next-v2).
316
+ with a deployed Publisher (currently: PCF-devnet).
316
317
  Requires --mnemonic (the signer must own the label).
317
318
  --unpublish Standalone mode: removes <domain> from the Publisher
318
319
  registry (Publisher.unpublish). Skips the deploy.
@@ -542,12 +543,21 @@ try {
542
543
  console.log(` ${display.map(s => s.toLowerCase()).join(" · ")}`);
543
544
  }
544
545
  },
545
- confirmPhoneReady: ({ label, attempt }) => new Promise((resolve, reject) => {
546
- if (attempt >= 2) {
546
+ // #194: `approvalBudgetMs` is threaded through from PHONE_APPROVAL_MS
547
+ // (src/dotns.ts) rather than hardcoded here, so the disclosed number can
548
+ // never drift from the constant that actually governs the deadline.
549
+ // `reason: "silence"` marks a watcher-silence re-arm (no response within
550
+ // the budget) as distinct from the pre-existing verifyEffect-false-negative
551
+ // re-sign case (undefined/"resign", still rendered off `attempt`).
552
+ confirmPhoneReady: ({ label, attempt, approvalBudgetMs, reason }) => new Promise((resolve, reject) => {
553
+ const budgetSeconds = Math.round(approvalBudgetMs / 1000);
554
+ if (reason === "silence") {
555
+ console.log(`\n still nothing from your phone — press Y to re-send, Ctrl-C to abort`);
556
+ } else if (attempt >= 2) {
547
557
  console.log(`\n Re-sign needed (attempt ${attempt}) — approve on your phone, then press Y to continue (Ctrl-C to abort).`);
548
558
  } else {
549
559
  console.log(`\n Check your phone → ${label}`);
550
- console.log(` Press Y when ready (Ctrl-C to abort):`);
560
+ console.log(` Press Y when ready — you'll have ~${budgetSeconds}s to approve on your phone (Ctrl-C to abort):`);
551
561
  }
552
562
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
553
563
  let settled = false;
@@ -556,7 +566,11 @@ try {
556
566
  const ask = () => {
557
567
  rl.question(" > ", (line) => {
558
568
  const answer = line.trim().toLowerCase();
559
- if (answer === "y" || answer === "") {
569
+ // #194: require an EXPLICIT y/yes bare Enter is the input most
570
+ // likely to come from something other than a deliberate keypress
571
+ // (buffered newline, stray Enter, piped stdin), and accepting it
572
+ // starts a fatal clock with nobody watching.
573
+ if (answer === "y" || answer === "yes") {
560
574
  settle(resolve);
561
575
  } else {
562
576
  ask();
@@ -9,12 +9,12 @@ import {
9
9
  getPeopleChainEndpoints,
10
10
  hasPersistedSession,
11
11
  resolveBulletinEndpoints
12
- } from "./chunk-KYBCFN3P.js";
12
+ } from "./chunk-6C3JLASN.js";
13
13
  import "./chunk-TSPERKUS.js";
14
- import "./chunk-CL7GQYYK.js";
14
+ import "./chunk-7CHUI4JX.js";
15
15
  import "./chunk-ZOC4GITL.js";
16
- import "./chunk-X7QKYH3U.js";
17
- import "./chunk-5HYWJO57.js";
16
+ import "./chunk-BBPA3Q5K.js";
17
+ import "./chunk-XFXTMHZX.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-CRNBI46S.js";
14
- import "./chunk-2M65ETRD.js";
15
- import "./chunk-X7QKYH3U.js";
16
- import "./chunk-5HYWJO57.js";
13
+ } from "./chunk-PK3T6C6T.js";
14
+ import "./chunk-VSLW6WXT.js";
15
+ import "./chunk-BBPA3Q5K.js";
16
+ import "./chunk-XFXTMHZX.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-SJPSTDCN.js";
7
- import "./chunk-X7QKYH3U.js";
8
- import "./chunk-5HYWJO57.js";
6
+ } from "./chunk-466FDICA.js";
7
+ import "./chunk-BBPA3Q5K.js";
8
+ import "./chunk-XFXTMHZX.js";
9
9
  export {
10
10
  ChainHealthSampler,
11
11
  DEFAULT_WINDOW_BLOCKS,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  validateProductConfig
3
- } from "./chunk-YXMWCUXP.js";
3
+ } from "./chunk-D6AYE6KA.js";
4
4
  import {
5
5
  NonRetryableError
6
6
  } from "./chunk-ZOC4GITL.js";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  captureWarning
3
- } from "./chunk-X7QKYH3U.js";
3
+ } from "./chunk-BBPA3Q5K.js";
4
4
 
5
5
  // src/chunk-probe.ts
6
6
  import { Twox128, Blake2128Concat, decAnyMetadata, unifyMetadata } from "@polkadot-api/substrate-bindings";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  emitIntervalSpan,
3
3
  setDeployAttribute
4
- } from "./chunk-X7QKYH3U.js";
4
+ } from "./chunk-BBPA3Q5K.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-CL7GQYYK.js";
6
+ } from "./chunk-7CHUI4JX.js";
7
7
  import {
8
8
  VERSION
9
- } from "./chunk-X7QKYH3U.js";
9
+ } from "./chunk-BBPA3Q5K.js";
10
10
 
11
11
  // src/auth-config.ts
12
12
  import { existsSync, readdirSync } from "fs";
@@ -20,7 +20,7 @@ var environments_default = {
20
20
  backend: "https://polkadot-app-stg.parity.io/",
21
21
  ipfs: "https://previewnet.substrate.dev",
22
22
  webGateway: "testnet.li",
23
- tld: "test",
23
+ tld: "testnet",
24
24
  autoAccountMapping: true,
25
25
  nativeToEthRatio: 1e8,
26
26
  registerStorageDeposit: 2e12,
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  package_default,
3
3
  writeRunState
4
- } from "./chunk-5HYWJO57.js";
4
+ } from "./chunk-XFXTMHZX.js";
5
5
 
6
6
  // src/memory-report.ts
7
7
  import * as fs2 from "fs";
@@ -7,7 +7,7 @@ var KIND_WORKER = "worker";
7
7
  var EXECUTABLE_KINDS = [KIND_APP, KIND_WIDGET, KIND_FUNDING, KIND_WORKER];
8
8
  var FUNDING_MODES = ["CARD", "BANK", "CRYPTO"];
9
9
  var LABEL = String.raw`(?!-)[a-z0-9-]{1,63}(?<!-)`;
10
- var KNOWN_TLDS = ["dot", "paseo", "test"];
10
+ var KNOWN_TLDS = ["dot", "paseo", "test", "testnet"];
11
11
  var DOMAIN_RE = new RegExp(`^${LABEL}(\\.${LABEL})*\\.(?:${KNOWN_TLDS.join("|")})$`, "i");
12
12
  function isPlainObject(value) {
13
13
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  preflightSssAllowance
3
- } from "./chunk-4T6LXSOJ.js";
3
+ } from "./chunk-SWD774NU.js";
4
4
  import {
5
5
  statementSigningAccount
6
6
  } from "./chunk-GRPLHUYC.js";
@@ -26,18 +26,18 @@ import {
26
26
  } from "./chunk-S7EM5VMW.js";
27
27
  import {
28
28
  setDeployContext
29
- } from "./chunk-CRNBI46S.js";
29
+ } from "./chunk-PK3T6C6T.js";
30
30
  import {
31
31
  getBestBlockNumber,
32
32
  probeChunks,
33
33
  probeFinalityGap
34
- } from "./chunk-CD2NN5LL.js";
34
+ } from "./chunk-33SYDRVZ.js";
35
35
  import {
36
36
  packSection
37
37
  } from "./chunk-C2TS5MER.js";
38
38
  import {
39
39
  resolveStorageSigner
40
- } from "./chunk-EKBGOA24.js";
40
+ } from "./chunk-OWCCOKOP.js";
41
41
  import {
42
42
  createSlotAccountSigner,
43
43
  requestResourceAllocation
@@ -46,7 +46,7 @@ import {
46
46
  STALE_SESSION_MESSAGE,
47
47
  getPeopleChainEndpoints,
48
48
  hasPersistedSession
49
- } from "./chunk-KYBCFN3P.js";
49
+ } from "./chunk-6C3JLASN.js";
50
50
  import {
51
51
  CLI_NAME
52
52
  } from "./chunk-TSPERKUS.js";
@@ -62,7 +62,7 @@ import {
62
62
  parseDomainName,
63
63
  popStatusName,
64
64
  verifyNonceAdvanced
65
- } from "./chunk-ZPQO4PY2.js";
65
+ } from "./chunk-SY2KD7SN.js";
66
66
  import {
67
67
  derivePoolAccounts,
68
68
  detectTestnet,
@@ -77,13 +77,13 @@ import {
77
77
  getPopSelfServeConfig,
78
78
  loadEnvironments,
79
79
  resolveEndpoints
80
- } from "./chunk-CL7GQYYK.js";
80
+ } from "./chunk-7CHUI4JX.js";
81
81
  import {
82
82
  NonRetryableError
83
83
  } from "./chunk-ZOC4GITL.js";
84
84
  import {
85
85
  ChainHealthSampler
86
- } from "./chunk-SJPSTDCN.js";
86
+ } from "./chunk-466FDICA.js";
87
87
  import {
88
88
  VERSION,
89
89
  captureWarning,
@@ -97,7 +97,7 @@ import {
97
97
  truncateAddress,
98
98
  withDeploySpan,
99
99
  withSpan
100
- } from "./chunk-X7QKYH3U.js";
100
+ } from "./chunk-BBPA3Q5K.js";
101
101
 
102
102
  // src/merkle.ts
103
103
  import * as fs2 from "fs";
@@ -4,10 +4,10 @@ import {
4
4
  } from "./chunk-5FLTDWWP.js";
5
5
  import {
6
6
  DOT_PRODUCT_ID
7
- } from "./chunk-KYBCFN3P.js";
7
+ } from "./chunk-6C3JLASN.js";
8
8
  import {
9
9
  DEFAULT_MNEMONIC
10
- } from "./chunk-ZPQO4PY2.js";
10
+ } from "./chunk-SY2KD7SN.js";
11
11
 
12
12
  // src/deploy-actors.ts
13
13
  var DEFAULT_WORKER_SURI = DEFAULT_MNEMONIC;
@@ -2,15 +2,15 @@ import {
2
2
  classifyErrorArea,
3
3
  isInteractive,
4
4
  promptYesNo
5
- } from "./chunk-2M65ETRD.js";
5
+ } from "./chunk-VSLW6WXT.js";
6
6
  import {
7
7
  VERSION,
8
8
  getCurrentSentryTraceId,
9
9
  resolveIssueRepoSlug
10
- } from "./chunk-X7QKYH3U.js";
10
+ } from "./chunk-BBPA3Q5K.js";
11
11
  import {
12
12
  package_default
13
- } from "./chunk-5HYWJO57.js";
13
+ } from "./chunk-XFXTMHZX.js";
14
14
 
15
15
  // src/bug-report.ts
16
16
  import { execSync, execFileSync } from "child_process";
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-GRPLHUYC.js";
4
4
  import {
5
5
  DOT_DAPP_ID
6
- } from "./chunk-KYBCFN3P.js";
6
+ } from "./chunk-6C3JLASN.js";
7
7
 
8
8
  // src/sss-allowance-cache.ts
9
9
  import { mkdir, readFile, writeFile, unlink } from "fs/promises";
@@ -6,13 +6,13 @@ import {
6
6
  } from "./chunk-KJVRJOKC.js";
7
7
  import {
8
8
  validateContractAddresses
9
- } from "./chunk-CL7GQYYK.js";
9
+ } from "./chunk-7CHUI4JX.js";
10
10
  import {
11
11
  NonRetryableError
12
12
  } from "./chunk-ZOC4GITL.js";
13
13
  import {
14
14
  ChainHealthSampler
15
- } from "./chunk-SJPSTDCN.js";
15
+ } from "./chunk-466FDICA.js";
16
16
  import {
17
17
  captureWarning,
18
18
  markCodePath,
@@ -20,7 +20,7 @@ import {
20
20
  setDeploySentryTag,
21
21
  truncateAddress,
22
22
  withSpan
23
- } from "./chunk-X7QKYH3U.js";
23
+ } from "./chunk-BBPA3Q5K.js";
24
24
 
25
25
  // src/dotns.ts
26
26
  import crypto from "crypto";
@@ -143,6 +143,7 @@ var TX_TIMEOUT_MS = 9e4;
143
143
  var TX_CHAIN_TIME_BUDGET_MS = 18e4;
144
144
  var TX_WALL_CLOCK_CEILING_MS = 24e4;
145
145
  var TX_NO_PROGRESS_MS = 9e4;
146
+ var PHONE_APPROVAL_MS = 9e4;
146
147
  var DOTNS_BEST_BLOCK_GRACE_MS = parseInt(process.env.BULLETIN_DOTNS_BESTBLOCK_GRACE_MS ?? "60000", 10);
147
148
  var WS_HEARTBEAT_TIMEOUT_MS = 3e5;
148
149
  var DOTNS_TX_MAX_ATTEMPTS = 3;
@@ -174,12 +175,15 @@ function classifyTxRetryDecision(err) {
174
175
  if (lower.includes("transaction watcher silent")) return "retry";
175
176
  return "abort";
176
177
  }
178
+ var PhoneSilenceNonRetryableError = class extends NonRetryableError {
179
+ };
177
180
  function classifyWatcherSilentFastFail(err, isPhoneSigner) {
178
181
  if (err instanceof WatcherSilentNoEventError && isPhoneSigner === true) {
179
- return new NonRetryableError("No signature received from the phone \u2014 re-run when you can approve on your phone.");
182
+ return new PhoneSilenceNonRetryableError("No signature received from the phone \u2014 re-run when you can approve on your phone.");
180
183
  }
181
184
  return null;
182
185
  }
186
+ var PHONE_SILENCE_MAX_REARMS = 3;
183
187
  var DOTNS_RETRY_BASE_MS = 400;
184
188
  var DOTNS_RETRY_MAX_MS = 6e3;
185
189
  function dotnsRetryBackoffMs(attempt, rand = Math.random) {
@@ -236,7 +240,7 @@ function makeRetryStatusFilter(sink) {
236
240
  }
237
241
  var DEFAULT_MNEMONIC = "bottom drive obey lake curtain smoke basket hold race lonely fit walk";
238
242
  var DEFAULT_TLD = "dot";
239
- var KNOWN_TLDS = ["dot", "paseo", "test"];
243
+ var KNOWN_TLDS = ["dot", "paseo", "test", "testnet"];
240
244
  function normalizeOnChainTld(raw) {
241
245
  return raw.startsWith(".") ? raw.slice(1) : raw;
242
246
  }
@@ -418,7 +422,7 @@ var PUBLISHER_ABI = [
418
422
  ];
419
423
  var PublisherNotSupportedError = class extends Error {
420
424
  constructor(envName) {
421
- super(`Publisher contract is not configured for environment '${envName}'. Use an env that has a deployed Publisher (currently: paseo-next-v2).`);
425
+ super(`Publisher contract is not configured for environment '${envName}'. Use an env that has a deployed Publisher (currently: PCF-devnet).`);
422
426
  this.name = "PublisherNotSupportedError";
423
427
  }
424
428
  };
@@ -572,6 +576,19 @@ function convertWeiToNative(weiValue) {
572
576
  function computeDomainTokenId(label, tld = DEFAULT_TLD) {
573
577
  return BigInt(namehash(`${label}.${tld}`));
574
578
  }
579
+ function computeSubnodeIds(sublabel, parentLabel, tld = DEFAULT_TLD) {
580
+ return {
581
+ parentNode: namehash(`${parentLabel}.${tld}`),
582
+ subnode: namehash(`${sublabel}.${parentLabel}.${tld}`)
583
+ };
584
+ }
585
+ function assertNotZeroRecipient(toH160, fullName) {
586
+ if (toH160.toLowerCase() === zeroAddress) {
587
+ throw new Error(
588
+ `Refusing to transfer ${fullName} to the zero address (${zeroAddress}): this would permanently burn it \u2014 there is no owner to recover it from afterward. Pass a real recipient with --to <0xH160>.`
589
+ );
590
+ }
591
+ }
575
592
  function countTrailingDigits(label) {
576
593
  let count = 0;
577
594
  for (let i = label.length - 1; i >= 0; i--) {
@@ -914,10 +931,11 @@ var ReviveClientWrapper = class _ReviveClientWrapper {
914
931
  let lastEventType = "(none)";
915
932
  let startChainTimeMs = null;
916
933
  let bestBlockIncluded = false;
934
+ const silenceDeadlineMs = opts.isPhoneSigner === true ? PHONE_APPROVAL_MS : TX_NO_PROGRESS_MS;
917
935
  const graceRawEnv = process.env.BULLETIN_DOTNS_BESTBLOCK_GRACE_MS;
918
936
  const bestBlockGraceMs = Math.min(
919
937
  graceRawEnv != null && graceRawEnv !== "" ? parseInt(graceRawEnv, 10) : DOTNS_BEST_BLOCK_GRACE_MS,
920
- TX_NO_PROGRESS_MS - 5e3
938
+ silenceDeadlineMs - 5e3
921
939
  );
922
940
  const poll = async () => {
923
941
  if (settled) return;
@@ -962,7 +980,7 @@ var ReviveClientWrapper = class _ReviveClientWrapper {
962
980
  return;
963
981
  }
964
982
  const silentMs = Date.now() - lastEventAt;
965
- if (silentMs > TX_NO_PROGRESS_MS) {
983
+ if (silentMs > silenceDeadlineMs) {
966
984
  statusCallback("failed");
967
985
  if (lastEventType === "(none)") {
968
986
  finish(reject)(new WatcherSilentNoEventError(silentMs));
@@ -1887,7 +1905,7 @@ var DotNS = class _DotNS {
1887
1905
  if (phoneLabel !== void 0) {
1888
1906
  await this._awaitPhoneReady(phoneLabel);
1889
1907
  }
1890
- return await withTimeout(
1908
+ const submitOnce = () => withTimeout(
1891
1909
  this.clientWrapper.submitTransaction(contractAddress, value, encodedCallData, this.substrateAddress, this.signer, statusCallback, {
1892
1910
  rpcs,
1893
1911
  useNoncePolling,
@@ -1902,11 +1920,26 @@ var DotNS = class _DotNS {
1902
1920
  // your phone / Press Y") before re-signing. Both _awaitPhoneReady and the
1903
1921
  // retry-loop guard no-op for non-phone signers (local/dev workers).
1904
1922
  isPhoneSigner: this._isPhoneSigner,
1905
- onResign: phoneLabel !== void 0 ? () => this._awaitPhoneReady(phoneLabel) : void 0
1923
+ // #194: thread the re-arm `reason` (undefined/"resign" for the #971
1924
+ // verifyEffect-false-negative case, "silence" for the watcher-silence
1925
+ // re-arm below) through to _awaitPhoneReady so confirmPhoneReady's
1926
+ // consumer can render the right prompt for each case.
1927
+ onResign: phoneLabel !== void 0 ? (_attempt, reason) => this._awaitPhoneReady(phoneLabel, reason) : void 0
1906
1928
  }),
1907
1929
  OPERATION_TIMEOUT_MS,
1908
1930
  functionName
1909
1931
  );
1932
+ if (phoneLabel === void 0 || !this._isPhoneSigner) return await submitOnce();
1933
+ const maxAttempts = 1 + PHONE_SILENCE_MAX_REARMS;
1934
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
1935
+ try {
1936
+ return await submitOnce();
1937
+ } catch (e) {
1938
+ if (!(e instanceof PhoneSilenceNonRetryableError) || attempt === maxAttempts) throw e;
1939
+ await this._awaitPhoneReady(phoneLabel, "silence");
1940
+ }
1941
+ }
1942
+ throw new Error("contractTransaction: unreachable \u2014 phone-silence re-arm loop exited without resolving");
1910
1943
  }
1911
1944
  async checkOwnership(label, ownerAddress = null) {
1912
1945
  this.ensureConnected();
@@ -1940,6 +1973,7 @@ var DotNS = class _DotNS {
1940
1973
  }) {
1941
1974
  this.ensureConnected();
1942
1975
  const validated = validateDomainLabel(label);
1976
+ assertNotZeroRecipient(toH160, `${validated}.${this._tld}`);
1943
1977
  const tokenId = computeDomainTokenId(validated, this._tld);
1944
1978
  const owner = await withTimeout(this.contractCall(this._contracts.DOTNS_REGISTRAR, DOTNS_REGISTRAR_TRANSFER_ABI, "ownerOf", [tokenId]), 3e4, "ownerOf");
1945
1979
  if (owner.toLowerCase() === toH160.toLowerCase()) {
@@ -1964,6 +1998,71 @@ var DotNS = class _DotNS {
1964
1998
  }
1965
1999
  return { status: "ok", txHash: txRes.kind === TX_KIND_HASH ? txRes.hash : void 0, feeWei };
1966
2000
  }
2001
+ /** Reassign an existing subname (e.g. `app.foo.<tld>`) to `toH160`.
2002
+ *
2003
+ * Subnames are NOT ERC-721 tokens, so they cannot go through
2004
+ * transferName/transferFrom — the registrar has no token for them. Instead
2005
+ * the *parent-domain* owner reassigns a subname via the registry's
2006
+ * setSubnodeOwner (the same call registerSubdomain uses to create it).
2007
+ * Authorisation is on parent ownership, not on the subname's current owner,
2008
+ * so the connected signer must own `${parentLabel}.${this._tld}`. Idempotent:
2009
+ * a no-op when the recipient already owns it.
2010
+ *
2011
+ * Every node here is derived via `namehash(\`...${this._tld}\`)` — the same
2012
+ * this._tld-templated convention as every other call site (see the comment
2013
+ * above computeDomainTokenId, ~line 1098) — deliberately NOT a hardcoded
2014
+ * ".dot" suffix. A second, manually-hardcoded node-derivation path is
2015
+ * exactly the bug class that burned production once already (registering
2016
+ * "ssoqedtuwf.paseo" minted namehash("ssoqedtuwf.paseo") but a hardcoded
2017
+ * ".dot" lookup queried namehash("ssoqedtuwf.dot") and reverted with
2018
+ * ERC721NonexistentToken after an 11 PAS mint had already succeeded). */
2019
+ async transferSubname(sublabel, parentLabel, toH160, statusCallback = () => {
2020
+ }) {
2021
+ this.ensureConnected();
2022
+ const fullName = `${sublabel}.${parentLabel}.${this._tld}`;
2023
+ assertNotZeroRecipient(toH160, fullName);
2024
+ const { parentNode, subnode } = computeSubnodeIds(sublabel, parentLabel, this._tld);
2025
+ const parentOwner = await withTimeout(
2026
+ this.contractCallNullable(this._contracts.DOTNS_REGISTRY, DOTNS_REGISTRY_ABI, "owner", [parentNode]),
2027
+ 3e4,
2028
+ "owner"
2029
+ );
2030
+ if (!parentOwner || parentOwner === zeroAddress) {
2031
+ throw new Error(`Cannot transfer ${fullName}: parent ${parentLabel}.${this._tld} is not registered.`);
2032
+ }
2033
+ if (parentOwner.toLowerCase() !== this.evmAddress.toLowerCase()) {
2034
+ throw new Error(
2035
+ `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).`
2036
+ );
2037
+ }
2038
+ const current = await withTimeout(
2039
+ this.contractCallNullable(this._contracts.DOTNS_REGISTRY, DOTNS_REGISTRY_ABI, "owner", [subnode]),
2040
+ 3e4,
2041
+ "owner"
2042
+ );
2043
+ if (current && current.toLowerCase() === toH160.toLowerCase()) {
2044
+ statusCallback("already owned by recipient");
2045
+ return { status: "skipped-already-owned" };
2046
+ }
2047
+ const subnodeRecord = { parentNode, subLabel: sublabel, parentLabel, owner: toH160 };
2048
+ const txRes = await this.contractTransaction(
2049
+ this._contracts.DOTNS_REGISTRY,
2050
+ 0n,
2051
+ DOTNS_REGISTRY_ABI,
2052
+ "setSubnodeOwner",
2053
+ [subnodeRecord],
2054
+ statusCallback
2055
+ );
2056
+ const after = await withTimeout(
2057
+ this.contractCallNullable(this._contracts.DOTNS_REGISTRY, DOTNS_REGISTRY_ABI, "owner", [subnode]),
2058
+ 3e4,
2059
+ "owner"
2060
+ );
2061
+ if (!after || after.toLowerCase() !== toH160.toLowerCase()) {
2062
+ throw new Error(`Transfer of ${fullName} did not land: owner is ${after ?? "unset"}, expected ${toH160}.`);
2063
+ }
2064
+ return { status: "ok", txHash: txRes.kind === TX_KIND_HASH ? txRes.hash : void 0 };
2065
+ }
1967
2066
  async getUserPopStatus(ownerAddress = null) {
1968
2067
  if (this._userPopStatusOverrideForTest !== null) {
1969
2068
  const result = this._userPopStatusOverrideForTest;
@@ -1988,7 +2087,7 @@ var DotNS = class _DotNS {
1988
2087
  async checkSubdomainOwnership(sublabel, parentLabel) {
1989
2088
  this.ensureConnected();
1990
2089
  if (!this.clientWrapper) return { owned: false, owner: null };
1991
- const node = namehash(`${sublabel}.${parentLabel}.${this._tld}`);
2090
+ const { subnode: node } = computeSubnodeIds(sublabel, parentLabel, this._tld);
1992
2091
  try {
1993
2092
  const owner = await withTimeout(this.contractCallNullable(this._contracts.DOTNS_REGISTRY, DOTNS_REGISTRY_ABI, "owner", [node]), 3e4, "owner");
1994
2093
  if (!owner || owner === zeroAddress) return { owned: false, owner: null };
@@ -2003,8 +2102,7 @@ var DotNS = class _DotNS {
2003
2102
  this.ensureConnected();
2004
2103
  console.log(`
2005
2104
  Registering subdomain ${sublabel}.${parentLabel}.${this._tld}...`);
2006
- const parentNode = namehash(`${parentLabel}.${this._tld}`);
2007
- const subnodeNode = namehash(`${sublabel}.${parentLabel}.${this._tld}`);
2105
+ const { parentNode, subnode: subnodeNode } = computeSubnodeIds(sublabel, parentLabel, this._tld);
2008
2106
  const subnodeRecord = { parentNode, subLabel: sublabel, parentLabel, owner: this.evmAddress };
2009
2107
  const MAX_VERIFY_CHAIN_SECONDS = VERIFY_EFFECT_CHAIN_SECONDS;
2010
2108
  const POLL_INTERVAL_MS = 2e3;
@@ -3198,13 +3296,18 @@ var DotNS = class _DotNS {
3198
3296
  * in-process).
3199
3297
  * After the gate resolves, fires onPhoneSigningRequired (the "check your
3200
3298
  * phone" notification) so the user knows the request is now being sent.
3299
+ *
3300
+ * `reason` (#194): passed straight through to confirmPhoneReady's context so
3301
+ * the consumer can distinguish a watcher-silence re-arm ("silence") from the
3302
+ * pre-existing verifyEffect-false-negative re-sign case (undefined/"resign",
3303
+ * which keeps rendering off `attempt` as before).
3201
3304
  */
3202
- async _awaitPhoneReady(label) {
3305
+ async _awaitPhoneReady(label, reason) {
3203
3306
  if (!this._isPhoneSigner) return;
3204
3307
  const attempt = (this._phoneSignatureAttempts.get(label) ?? 0) + 1;
3205
3308
  this._phoneSignatureAttempts.set(label, attempt);
3206
3309
  if (this._confirmPhoneReady) {
3207
- await this._confirmPhoneReady({ label, attempt, total: this._phoneSignatureTotal });
3310
+ await this._confirmPhoneReady({ label, attempt, total: this._phoneSignatureTotal, approvalBudgetMs: PHONE_APPROVAL_MS, reason });
3208
3311
  }
3209
3312
  this._onPhoneSigningRequired?.(label);
3210
3313
  }
@@ -3252,6 +3355,7 @@ export {
3252
3355
  TX_CHAIN_TIME_BUDGET_MS,
3253
3356
  TX_WALL_CLOCK_CEILING_MS,
3254
3357
  TX_NO_PROGRESS_MS,
3358
+ PHONE_APPROVAL_MS,
3255
3359
  DOTNS_BEST_BLOCK_GRACE_MS,
3256
3360
  WS_HEARTBEAT_TIMEOUT_MS,
3257
3361
  DOTNS_TX_MAX_ATTEMPTS,
@@ -3261,7 +3365,9 @@ export {
3261
3365
  NONCE_ADVANCE_VERIFY_RETRY_INTERVAL_MS,
3262
3366
  verifyEffectWithGrace,
3263
3367
  classifyTxRetryDecision,
3368
+ PhoneSilenceNonRetryableError,
3264
3369
  classifyWatcherSilentFastFail,
3370
+ PHONE_SILENCE_MAX_REARMS,
3265
3371
  dotnsRetryBackoffMs,
3266
3372
  withRetry,
3267
3373
  shouldRetryTxAttempt,
@@ -3289,6 +3395,8 @@ export {
3289
3395
  __formatContractDryRunFailureForTest,
3290
3396
  convertWeiToNative,
3291
3397
  computeDomainTokenId,
3398
+ computeSubnodeIds,
3399
+ assertNotZeroRecipient,
3292
3400
  countTrailingDigits,
3293
3401
  stripTrailingDigits,
3294
3402
  sanitizeDomainLabel,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  VERSION
3
- } from "./chunk-X7QKYH3U.js";
3
+ } from "./chunk-BBPA3Q5K.js";
4
4
 
5
5
  // src/version-check.ts
6
6
  import { execSync, execFileSync } from "child_process";
@@ -9,19 +9,19 @@ import {
9
9
  setBulletinEndpoints,
10
10
  storeDirectory,
11
11
  storeFile
12
- } from "./chunk-JZNSDAZM.js";
12
+ } from "./chunk-OORLC7SM.js";
13
13
  import {
14
14
  DEFAULT_TLD,
15
15
  DotNS,
16
16
  TX_KIND_SKIPPED,
17
17
  stripTldSuffix
18
- } from "./chunk-ZPQO4PY2.js";
18
+ } from "./chunk-SY2KD7SN.js";
19
19
  import {
20
20
  DEFAULT_ENV_ID,
21
21
  getPopSelfServeConfig,
22
22
  loadEnvironments,
23
23
  resolveEndpoints
24
- } from "./chunk-CL7GQYYK.js";
24
+ } from "./chunk-7CHUI4JX.js";
25
25
  import {
26
26
  NonRetryableError
27
27
  } from "./chunk-ZOC4GITL.js";
@@ -29,7 +29,7 @@ import {
29
29
  setDeployAttribute,
30
30
  withManifestSpan,
31
31
  withSpan
32
- } from "./chunk-X7QKYH3U.js";
32
+ } from "./chunk-BBPA3Q5K.js";
33
33
 
34
34
  // src/manifest/publish.ts
35
35
  import * as fs from "fs/promises";
@@ -6,7 +6,7 @@ import * as path from "path";
6
6
  // package.json
7
7
  var package_default = {
8
8
  name: "bulletin-deploy",
9
- version: "0.15.2",
9
+ version: "0.16.0-rc.2",
10
10
  private: false,
11
11
  repository: {
12
12
  type: "git",
@@ -56,7 +56,7 @@ var package_default = {
56
56
  "refresh-environments": "node scripts/refresh-environments.mjs",
57
57
  postinstall: "patch-package || true",
58
58
  prepare: "npm run build",
59
- test: "npm run build && node --test test/test.js test/cli-help.test.js test/helpers/e2e-helpers.test.js test/environments.test.js test/refresh-environments.test.js test/chunk-sharing-report.test.js test/dotns-deployments-parser.test.js test/dotns-token-id.test.js test/product-manifest.test.js test/cache-savings-totals.test.js test/error-pattern-signature.test.js test/exit-codes.test.js test/probe-env-health.test.js test/e2e-chain-calls.test.js test/auth-config.test.js test/whoami.test.js test/login.test.js test/logout.test.js test/auth-resolve.test.js test/storage-signer.test.js test/spinner.test.js test/sss-allowance.test.js test/sss-allowance-cache.test.js test/dotns-transfer.test.js test/deploy-actors.test.js test/transfer-command.test.js test/dotns-register-fee.test.js test/deploy-label-ordering.test.js test/benign-teardown.test.js test/verify-baked-dsn.test.js test/test-release-retry-wrapper.js && npm run test:vendor",
59
+ test: "npm run build && node --test test/test.js test/cli-help.test.js test/helpers/e2e-helpers.test.js test/environments.test.js test/refresh-environments.test.js test/chunk-sharing-report.test.js test/dotns-deployments-parser.test.js test/dotns-token-id.test.js test/dotns-subnode-id.test.js test/product-manifest.test.js test/cache-savings-totals.test.js test/error-pattern-signature.test.js test/exit-codes.test.js test/probe-env-health.test.js test/e2e-chain-calls.test.js test/auth-config.test.js test/whoami.test.js test/login.test.js test/logout.test.js test/auth-resolve.test.js test/storage-signer.test.js test/spinner.test.js test/sss-allowance.test.js test/sss-allowance-cache.test.js test/dotns-transfer.test.js test/deploy-actors.test.js test/transfer-command.test.js test/dotns-register-fee.test.js test/deploy-label-ordering.test.js test/benign-teardown.test.js test/verify-baked-dsn.test.js test/test-release-retry-wrapper.js && npm run test:vendor",
60
60
  "test:e2e": "npm run build && node --test test/e2e.test.js",
61
61
  "test:e2e:smoke": "bash scripts/e2e-pass.sh smoke",
62
62
  "test:e2e:pr": "bash scripts/e2e-pass.sh pr",