bulletin-deploy 0.7.2 → 0.7.4

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.
@@ -1,13 +1,14 @@
1
1
  import {
2
2
  setDeployContext
3
- } from "./chunk-LMSMDKVU.js";
3
+ } from "./chunk-YYULF2JX.js";
4
4
  import {
5
5
  DotNS,
6
6
  TX_TIMEOUT_MS,
7
7
  fetchNonce,
8
+ parseDomainName,
8
9
  popStatusName,
9
- validateDomainLabel
10
- } from "./chunk-NEV6WTYM.js";
10
+ verifyNonceAdvanced
11
+ } from "./chunk-YREZFNCC.js";
11
12
  import {
12
13
  MirrorSkipped,
13
14
  mirrorToGitHubPages,
@@ -26,7 +27,7 @@ import {
26
27
  truncateAddress,
27
28
  withDeploySpan,
28
29
  withSpan
29
- } from "./chunk-4FUUYJP2.js";
30
+ } from "./chunk-DHQ3JGF4.js";
30
31
  import {
31
32
  merkleizeJS
32
33
  } from "./chunk-B7GUYYAN.js";
@@ -48,7 +49,7 @@ import { sha256 } from "@noble/hashes/sha256";
48
49
  import { blake2b } from "@noble/hashes/blake2b";
49
50
  import { createClient as createPolkadotClient, Enum } from "polkadot-api";
50
51
  import { Binary } from "@polkadot-api/substrate-bindings";
51
- import { getWsProvider } from "polkadot-api/ws-provider";
52
+ import { getWsProvider, WsEvent } from "polkadot-api/ws-provider";
52
53
  import { withPolkadotSdkCompat } from "polkadot-api/polkadot-sdk-compat";
53
54
  import { CID } from "multiformats/cid";
54
55
  import { create as createMultihash } from "multiformats/hashes/digest";
@@ -75,12 +76,23 @@ function friendlyChainError(msg) {
75
76
  }
76
77
  var DEFAULT_BULLETIN_RPC = "wss://paseo-bulletin-rpc.polkadot.io";
77
78
  var DEFAULT_POOL_SIZE = 10;
78
- var BULLETIN_RPC = DEFAULT_BULLETIN_RPC;
79
+ var BULLETIN_ENDPOINTS = [DEFAULT_BULLETIN_RPC];
79
80
  var POOL_SIZE = DEFAULT_POOL_SIZE;
81
+ var _deployRpcFailedOver = false;
82
+ function makeBulletinStatusHandler(primary) {
83
+ return (s) => {
84
+ if (s.type === WsEvent.CONNECTED && s.uri !== primary) {
85
+ _deployRpcFailedOver = true;
86
+ setDeployAttribute("deploy.rpc.failed_over", "true");
87
+ captureWarning("Bulletin RPC failover", { from: primary, to: s.uri });
88
+ }
89
+ };
90
+ }
80
91
  var CHUNK_SIZE = 2 * 1024 * 1024;
81
92
  var MAX_FILE_SIZE = 8 * 1024 * 1024;
82
93
  var MAX_RECONNECTIONS = parseInt(process.env.BULLETIN_MAX_RECONNECTIONS ?? "3", 10);
83
94
  var CHUNK_TIMEOUT_MS = parseInt(process.env.BULLETIN_CHUNK_TIMEOUT_MS ?? "180000", 10);
95
+ var CHUNK_MORTALITY_PERIOD = 16;
84
96
  var RETRY_BASE_DELAY_MS = 2e3;
85
97
  var RETRY_MAX_DELAY_MS = 15e3;
86
98
  var WS_HEARTBEAT_TIMEOUT_MS = 3e5;
@@ -147,8 +159,12 @@ function toHashingEnum(mhCode) {
147
159
  }
148
160
  }
149
161
  async function getProvider() {
150
- console.log(` Connecting to Bulletin: ${BULLETIN_RPC}`);
151
- const client = createPolkadotClient(withPolkadotSdkCompat(getWsProvider(BULLETIN_RPC, { heartbeatTimeout: WS_HEARTBEAT_TIMEOUT_MS })));
162
+ const primary = BULLETIN_ENDPOINTS[0];
163
+ console.log(` Connecting to Bulletin: ${primary}`);
164
+ const client = createPolkadotClient(withPolkadotSdkCompat(getWsProvider(
165
+ BULLETIN_ENDPOINTS,
166
+ { heartbeatTimeout: WS_HEARTBEAT_TIMEOUT_MS, onStatusChanged: makeBulletinStatusHandler(primary) }
167
+ )));
152
168
  const unsafeApi = client.getUnsafeApi();
153
169
  try {
154
170
  await cryptoWaitReady();
@@ -159,10 +175,10 @@ async function getProvider() {
159
175
  if (!selected) {
160
176
  const best = authorizations.reduce((a, b) => a.transactions > b.transactions ? a : b);
161
177
  console.log(` All pool accounts low on capacity, auto-authorizing account ${best.index}...`);
162
- await ensureAuthorized(unsafeApi, best.address, BULLETIN_RPC, `pool account ${best.index}`);
178
+ await ensureAuthorized(unsafeApi, best.address, BULLETIN_ENDPOINTS[0], `pool account ${best.index}`);
163
179
  selected = best;
164
180
  } else {
165
- await ensureAuthorized(unsafeApi, selected.address, BULLETIN_RPC, `pool account ${selected.index}`);
181
+ await ensureAuthorized(unsafeApi, selected.address, BULLETIN_ENDPOINTS[0], `pool account ${selected.index}`);
166
182
  }
167
183
  console.log(` Using pool account ${selected.index}: ${selected.address}`);
168
184
  setDeployAttribute("deploy.signer.mode", "pool");
@@ -175,8 +191,12 @@ async function getProvider() {
175
191
  }
176
192
  }
177
193
  async function getDirectProvider(mnemonic, derivationPath = "") {
178
- console.log(` Connecting to Bulletin: ${BULLETIN_RPC}`);
179
- const client = createPolkadotClient(withPolkadotSdkCompat(getWsProvider(BULLETIN_RPC, { heartbeatTimeout: WS_HEARTBEAT_TIMEOUT_MS })));
194
+ const primary = BULLETIN_ENDPOINTS[0];
195
+ console.log(` Connecting to Bulletin: ${primary}`);
196
+ const client = createPolkadotClient(withPolkadotSdkCompat(getWsProvider(
197
+ BULLETIN_ENDPOINTS,
198
+ { heartbeatTimeout: WS_HEARTBEAT_TIMEOUT_MS, onStatusChanged: makeBulletinStatusHandler(primary) }
199
+ )));
180
200
  const unsafeApi = client.getUnsafeApi();
181
201
  const { signer, ss58 } = deriveRootSigner(mnemonic, derivationPath);
182
202
  console.log(` Using direct signer: ${ss58}${derivationPath ? ` (path: ${derivationPath})` : ""}`);
@@ -195,8 +215,9 @@ async function getDirectProvider(mnemonic, derivationPath = "") {
195
215
  return { client, unsafeApi, signer, ss58 };
196
216
  }
197
217
  var MAX_BEST_CHAIN_DROPS = 5;
198
- function watchTransaction(tx, signer, txOpts, onSuccess, { label = "transaction", rpc, senderSS58, expectedNonce, timeoutMs } = {}) {
218
+ function watchTransaction(tx, signer, txOpts, onSuccess, { label = "transaction", rpc, senderSS58, expectedNonce, timeoutMs, fetchNonce: fetchNonceOverride } = {}) {
199
219
  const timeout = timeoutMs ?? TX_TIMEOUT_MS;
220
+ const _fetchNonce = fetchNonceOverride ?? fetchNonce;
200
221
  return new Promise((resolve2, reject) => {
201
222
  let settled = false;
202
223
  let sub;
@@ -211,14 +232,15 @@ function watchTransaction(tx, signer, txOpts, onSuccess, { label = "transaction"
211
232
  }
212
233
  fn(...args);
213
234
  };
214
- const tryNonceFallback = async () => {
235
+ const tryNonceFallback = async (subscriptionErrored) => {
215
236
  if (!rpc || !senderSS58 || expectedNonce == null) return false;
216
237
  try {
217
- const currentNonce = await fetchNonce(rpc, senderSS58);
238
+ const endpoints = Array.isArray(rpc) ? rpc : [rpc];
239
+ const verified = await verifyNonceAdvanced(endpoints, senderSS58, expectedNonce);
218
240
  if (settled) return true;
219
- if (currentNonce > expectedNonce) {
220
- console.log(` ${label}: nonce advanced (${expectedNonce} -> ${currentNonce}), tx was included`);
221
- settle(resolve2)({ value: onSuccess(), viaFallback: true });
241
+ if (verified.advanced) {
242
+ console.log(` ${label}: nonce advanced past ${expectedNonce} (witnessed by ${verified.witnessRpc}), tx was included`);
243
+ settle(resolve2)({ value: onSuccess(), viaFallback: true, subscriptionError: subscriptionErrored });
222
244
  return true;
223
245
  }
224
246
  } catch (e) {
@@ -229,7 +251,7 @@ function watchTransaction(tx, signer, txOpts, onSuccess, { label = "transaction"
229
251
  };
230
252
  const timer = setTimeout(async () => {
231
253
  if (settled) return;
232
- if (await tryNonceFallback()) return;
254
+ if (await tryNonceFallback(false)) return;
233
255
  settle(reject)(new Error(`${label} timed out after ${timeout / 1e3}s waiting for block confirmation`));
234
256
  }, timeout);
235
257
  sub = tx.signSubmitAndWatch(signer, txOpts).subscribe({
@@ -237,7 +259,7 @@ function watchTransaction(tx, signer, txOpts, onSuccess, { label = "transaction"
237
259
  if (event.type === "txBestBlocksState") {
238
260
  if (event.found) {
239
261
  if (event.ok) {
240
- settle(resolve2)({ value: onSuccess(event), viaFallback: false });
262
+ settle(resolve2)({ value: onSuccess(event), viaFallback: false, subscriptionError: false });
241
263
  } else {
242
264
  settle(reject)(new Error(`${label} dispatch error`));
243
265
  }
@@ -245,7 +267,7 @@ function watchTransaction(tx, signer, txOpts, onSuccess, { label = "transaction"
245
267
  dropCount++;
246
268
  if (dropCount >= MAX_BEST_CHAIN_DROPS) {
247
269
  console.log(` ${label}: tx dropped ${dropCount} times, checking nonce...`);
248
- if (await tryNonceFallback()) return;
270
+ if (await tryNonceFallback(true)) return;
249
271
  settle(reject)(new Error(`${label} tx dropped from best chain ${dropCount} times`));
250
272
  } else {
251
273
  console.log(` ${label}: tx dropped from best chain (${dropCount}/${MAX_BEST_CHAIN_DROPS}), waiting...`);
@@ -260,16 +282,16 @@ function watchTransaction(tx, signer, txOpts, onSuccess, { label = "transaction"
260
282
  });
261
283
  });
262
284
  }
263
- async function storeChunk(unsafeApi, signer, chunkBytes, nonce, ss58) {
285
+ async function storeChunk(unsafeApi, signer, chunkBytes, nonce, ss58, opts = {}) {
264
286
  const hashCode = 18;
265
287
  const cid = createCID(chunkBytes, CID_CONFIG.codec, hashCode);
266
288
  const tx = unsafeApi.tx.TransactionStorage.store_with_cid_config({ cid: { codec: BigInt(CID_CONFIG.codec), hashing: toHashingEnum(hashCode) }, data: Binary.fromBytes(chunkBytes) });
267
- const txOpts = { mortality: { mortal: true, period: 256 }, nonce };
268
- const { value, viaFallback } = await watchTransaction(tx, signer, txOpts, () => {
289
+ const txOpts = { mortality: { mortal: true, period: CHUNK_MORTALITY_PERIOD }, nonce };
290
+ const { value, viaFallback, subscriptionError } = await watchTransaction(tx, signer, txOpts, () => {
269
291
  console.log(` CID: ${cid.toString()}`);
270
292
  return { cid, len: chunkBytes.length };
271
- }, { label: `chunk(nonce:${nonce})`, rpc: BULLETIN_RPC, senderSS58: ss58, expectedNonce: nonce, timeoutMs: CHUNK_TIMEOUT_MS });
272
- return { ...value, viaFallback };
293
+ }, { label: `chunk(nonce:${nonce})`, rpc: BULLETIN_ENDPOINTS, senderSS58: ss58, expectedNonce: nonce, timeoutMs: CHUNK_TIMEOUT_MS, fetchNonce: opts.fetchNonce });
294
+ return { ...value, viaFallback, subscriptionError };
273
295
  }
274
296
  async function storeFile(contentBytes, { client: existingClient, unsafeApi: existingApi, signer: existingSigner } = {}) {
275
297
  console.log(`
@@ -305,7 +327,8 @@ async function storeFile(contentBytes, { client: existingClient, unsafeApi: exis
305
327
  throw e;
306
328
  }
307
329
  }
308
- async function storeChunkedContent(chunks, { client: existingClient, unsafeApi: existingApi, signer: existingSigner, ss58: existingSS58, reconnect } = {}) {
330
+ async function storeChunkedContent(chunks, { client: existingClient, unsafeApi: existingApi, signer: existingSigner, ss58: existingSS58, reconnect, fetchNonce: fetchNonceOverride } = {}) {
331
+ const _fetchNonce = fetchNonceOverride ?? fetchNonce;
309
332
  console.log(`
310
333
  Chunks: ${chunks.length}`);
311
334
  const totalBytes = chunks.reduce((s, c) => s + c.length, 0);
@@ -337,7 +360,7 @@ async function storeChunkedContent(chunks, { client: existingClient, unsafeApi:
337
360
  Account has insufficient authorization for this upload (need ${requiredTxs} txs / ${(totalBytes / 1e6).toFixed(1)}MB, have ${txsRemaining} txs / ${Number(bytesRemaining) / 1e6}MB)`);
338
361
  console.log(` Attempting to re-authorize with Alice...`);
339
362
  try {
340
- await ensureAuthorized(unsafeApi, ss58, BULLETIN_RPC, void 0, { txs: requiredTxs, bytes: requiredBytes });
363
+ await ensureAuthorized(unsafeApi, ss58, BULLETIN_ENDPOINTS[0], void 0, { txs: requiredTxs, bytes: requiredBytes });
341
364
  console.log(` Re-authorization successful`);
342
365
  } catch (e) {
343
366
  throw new NonRetryableError(`Account ${ss58} has insufficient Bulletin authorization quota and auto-authorization via Alice failed (${e.message}). Authorize the account on-chain.`);
@@ -368,7 +391,7 @@ async function storeChunkedContent(chunks, { client: existingClient, unsafeApi:
368
391
  sampleMemory(`reconnect_${reconnectionsUsed}_after`);
369
392
  }
370
393
  try {
371
- let startNonce = await fetchNonce(BULLETIN_RPC, ss58);
394
+ let startNonce = await _fetchNonce(BULLETIN_ENDPOINTS, ss58);
372
395
  console.log(` Starting nonce: ${startNonce}`);
373
396
  const BATCH_SIZE = 2;
374
397
  const MAX_CHUNK_RETRIES = 3;
@@ -394,7 +417,7 @@ async function storeChunkedContent(chunks, { client: existingClient, unsafeApi:
394
417
  }
395
418
  const nonce = assignedNonces.get(i);
396
419
  console.log(` [${i + 1}/${chunks.length}] ${(chunkData.length / 1024 / 1024).toFixed(2)} MB (nonce: ${nonce})`);
397
- return storeChunk(unsafeApi, signer, chunkData, nonce, ss58);
420
+ return storeChunk(unsafeApi, signer, chunkData, nonce, ss58, { fetchNonce: fetchNonceOverride });
398
421
  });
399
422
  const results = await Promise.allSettled(batchPromises);
400
423
  results.forEach((r, j) => {
@@ -404,11 +427,11 @@ async function storeChunkedContent(chunks, { client: existingClient, unsafeApi:
404
427
  }
405
428
  });
406
429
  const failures = results.map((r, j) => r.status === "rejected" ? { index: batchIndices[j], chunkData: batchChunks[j], error: r.reason } : null).filter(Boolean);
407
- const succeededViaFallback = results.some((r) => r.status === "fulfilled" && r.value.viaFallback);
408
- const needsReconnect = failures.some((f) => isConnectionError(f.error)) || succeededViaFallback;
430
+ const subscriptionErrored = results.some((r) => r.status === "fulfilled" && r.value.subscriptionError);
431
+ const needsReconnect = failures.some((f) => isConnectionError(f.error)) || subscriptionErrored;
409
432
  if (needsReconnect && reconnect && reconnectionsUsed < MAX_RECONNECTIONS) {
410
433
  await doReconnect();
411
- const currentNonce = await fetchNonce(BULLETIN_RPC, ss58);
434
+ const currentNonce = await _fetchNonce(BULLETIN_ENDPOINTS, ss58);
412
435
  for (const idx of batchIndices) {
413
436
  const chunkNonce = assignedNonces.get(idx);
414
437
  if (chunkNonce !== void 0 && chunkNonce < currentNonce && stored[idx] === null) {
@@ -439,7 +462,7 @@ async function storeChunkedContent(chunks, { client: existingClient, unsafeApi:
439
462
  }
440
463
  }
441
464
  try {
442
- const currentNonce = await fetchNonce(BULLETIN_RPC, ss58);
465
+ const currentNonce = await _fetchNonce(BULLETIN_ENDPOINTS, ss58);
443
466
  const originalNonce = assignedNonces.get(fail.index);
444
467
  if (originalNonce !== void 0 && originalNonce < currentNonce) {
445
468
  console.log(` Chunk ${fail.index + 1}: nonce ${originalNonce} consumed (current=${currentNonce}), treating as included`);
@@ -449,7 +472,7 @@ async function storeChunkedContent(chunks, { client: existingClient, unsafeApi:
449
472
  break;
450
473
  }
451
474
  const retryNonce = originalNonce ?? currentNonce;
452
- const result2 = await storeChunk(unsafeApi, signer, fail.chunkData, retryNonce, ss58);
475
+ const result2 = await storeChunk(unsafeApi, signer, fail.chunkData, retryNonce, ss58, { fetchNonce: fetchNonceOverride });
453
476
  stored[fail.index] = result2;
454
477
  assignedNonces.delete(fail.index);
455
478
  retried = true;
@@ -495,7 +518,7 @@ async function storeChunkedContent(chunks, { client: existingClient, unsafeApi:
495
518
  const MAX_ROOT_RETRIES = 3;
496
519
  let result;
497
520
  for (let rootAttempt = 1; rootAttempt <= MAX_ROOT_RETRIES; rootAttempt++) {
498
- const rootNonce = await fetchNonce(BULLETIN_RPC, ss58);
521
+ const rootNonce = await _fetchNonce(BULLETIN_ENDPOINTS, ss58);
499
522
  console.log(` Storing root node (nonce: ${rootNonce})...`);
500
523
  const rootTx = unsafeApi.tx.TransactionStorage.store_with_cid_config({ cid: { codec: BigInt(112), hashing: toHashingEnum(hashCode) }, data: Binary.fromBytes(dagBytes) });
501
524
  const rootTxOpts = { mortality: { mortal: true, period: 256 }, nonce: rootNonce };
@@ -504,9 +527,9 @@ async function storeChunkedContent(chunks, { client: existingClient, unsafeApi:
504
527
  console.log(` Root CID: ${rootCid.toString()}
505
528
  `);
506
529
  return rootCid.toString();
507
- }, { label: "root-node", rpc: BULLETIN_RPC, senderSS58: ss58, expectedNonce: rootNonce, timeoutMs: CHUNK_TIMEOUT_MS });
530
+ }, { label: "root-node", rpc: BULLETIN_ENDPOINTS, senderSS58: ss58, expectedNonce: rootNonce, timeoutMs: CHUNK_TIMEOUT_MS, fetchNonce: fetchNonceOverride });
508
531
  result = watchResult.value;
509
- if (watchResult.viaFallback && reconnect && reconnectionsUsed < MAX_RECONNECTIONS) {
532
+ if (watchResult.subscriptionError && reconnect && reconnectionsUsed < MAX_RECONNECTIONS) {
510
533
  await doReconnect();
511
534
  }
512
535
  break;
@@ -665,12 +688,14 @@ async function estimateUploadBytes(content) {
665
688
  }
666
689
  }
667
690
  async function deploy(content, domainName = null, options = {}) {
668
- BULLETIN_RPC = options.rpc ?? process.env.BULLETIN_RPC ?? DEFAULT_BULLETIN_RPC;
691
+ const userRpc = options.rpc ?? process.env.BULLETIN_RPC;
692
+ BULLETIN_ENDPOINTS = userRpc ? [userRpc, ...[DEFAULT_BULLETIN_RPC].filter((e) => e !== userRpc)] : [DEFAULT_BULLETIN_RPC];
693
+ _deployRpcFailedOver = false;
669
694
  POOL_SIZE = options.poolSize ?? parseInt(process.env.BULLETIN_POOL_SIZE ?? String(DEFAULT_POOL_SIZE), 10);
670
695
  initTelemetry();
671
696
  const randomSuffix = Math.floor(Math.random() * 100).toString().padStart(2, "0");
672
- const rawName = domainName ? domainName.replace(".dot", "") : `test-domain-${Date.now().toString(36)}${randomSuffix}`;
673
- const name = validateDomainLabel(rawName);
697
+ const parsed = domainName ? parseDomainName(domainName) : null;
698
+ const name = parsed ? parsed.label : `test-domain-${Date.now().toString(36)}${randomSuffix}`;
674
699
  return withDeploySpan(name, async () => {
675
700
  const deployTag = options.tag ?? process.env.DEPLOY_TAG;
676
701
  if (deployTag) {
@@ -714,26 +739,43 @@ async function deploy(content, domainName = null, options = {}) {
714
739
  );
715
740
  }
716
741
  console.log(` Account: mapped`);
717
- let dotnsPreflight;
718
- try {
719
- dotnsPreflight = await preflight.preflight(name);
720
- } finally {
721
- preflight.disconnect();
722
- }
723
- console.log(` DotNS: ${name}.dot classifies as ${popStatusName(dotnsPreflight.classification.status)}`);
724
- if (dotnsPreflight.canProceed) {
725
- const fromName = popStatusName(dotnsPreflight.userStatus);
726
- if (dotnsPreflight.needsPopUpgrade && dotnsPreflight.targetPopStatus !== void 0) {
727
- console.log(` PoP: ${fromName} \u2192 will upgrade to ${popStatusName(dotnsPreflight.targetPopStatus)}`);
728
- } else {
729
- console.log(` PoP: ${fromName} (no upgrade required)`);
742
+ if (parsed?.isSubdomain) {
743
+ try {
744
+ const { owned: subOwned } = await preflight.checkSubdomainOwnership(parsed.sublabel, parsed.parentLabel);
745
+ if (!subOwned) {
746
+ const { owned: parentOwned, owner: parentOwner } = await preflight.checkOwnership(parsed.parentLabel);
747
+ if (!parentOwned) {
748
+ throw new NonRetryableError(
749
+ `Cannot deploy ${parsed.fullName}: parent ${parsed.parentLabel}.dot is owned by ${parentOwner ?? "no one"}, not by this signer.`
750
+ );
751
+ }
752
+ }
753
+ } finally {
754
+ preflight.disconnect();
755
+ }
756
+ console.log(` Mode: subdomain (parent ${parsed.parentLabel}.dot owned by signer)`);
757
+ } else {
758
+ let dotnsPreflight;
759
+ try {
760
+ dotnsPreflight = await preflight.preflight(name);
761
+ } finally {
762
+ preflight.disconnect();
763
+ }
764
+ console.log(` DotNS: ${name}.dot classifies as ${popStatusName(dotnsPreflight.classification.status)}`);
765
+ if (dotnsPreflight.canProceed) {
766
+ const fromName = popStatusName(dotnsPreflight.userStatus);
767
+ if (dotnsPreflight.needsPopUpgrade && dotnsPreflight.targetPopStatus !== void 0) {
768
+ console.log(` PoP: ${fromName} \u2192 will upgrade to ${popStatusName(dotnsPreflight.targetPopStatus)}`);
769
+ } else {
770
+ console.log(` PoP: ${fromName} (no upgrade required)`);
771
+ }
772
+ console.log(` Domain: ${dotnsPreflight.plannedAction === "already-owned-by-us" ? "owned by you" : "available"}`);
773
+ }
774
+ if (!dotnsPreflight.canProceed) {
775
+ throw new NonRetryableError(
776
+ dotnsPreflight.reason ?? "DotNS preflight rejected the deploy; please check the label and signer."
777
+ );
730
778
  }
731
- console.log(` Domain: ${dotnsPreflight.plannedAction === "already-owned-by-us" ? "owned by you" : "available"}`);
732
- }
733
- if (!dotnsPreflight.canProceed) {
734
- throw new NonRetryableError(
735
- dotnsPreflight.reason ?? "DotNS preflight rejected the deploy; please check the label and signer."
736
- );
737
779
  }
738
780
  provider = await reconnect();
739
781
  const providerWithReconnect = { ...provider, reconnect };
@@ -745,7 +787,7 @@ async function deploy(content, domainName = null, options = {}) {
745
787
  const uploadBytes = Math.ceil(estimated * 1.2);
746
788
  const chunksNeeded = Math.max(1, Math.ceil(uploadBytes / CHUNK_SIZE));
747
789
  const needs = { txs: BigInt(chunksNeeded + 2), bytes: BigInt(uploadBytes) };
748
- await topUpBy(provider.ss58, BULLETIN_RPC, needs, "uploader");
790
+ await topUpBy(provider.ss58, BULLETIN_ENDPOINTS[0], needs, "uploader");
749
791
  }
750
792
  console.log("\n" + "=".repeat(60));
751
793
  console.log("Storage");
@@ -791,7 +833,7 @@ async function deploy(content, domainName = null, options = {}) {
791
833
  carBytes,
792
834
  cid: predictedCid,
793
835
  toolVersion: VERSION,
794
- bulletinRpc: options.rpc ?? process.env.BULLETIN_RPC ?? DEFAULT_BULLETIN_RPC,
836
+ bulletinRpc: BULLETIN_ENDPOINTS[0],
795
837
  encrypted: Boolean(options.password)
796
838
  }).catch((err) => err instanceof Error ? err : new Error(String(err)));
797
839
  }
@@ -844,17 +886,31 @@ async function deploy(content, domainName = null, options = {}) {
844
886
  console.log("\n" + "=".repeat(60));
845
887
  console.log("DotNS");
846
888
  console.log("=".repeat(60));
847
- await withSpan("deploy.dotns", "2. dotns", { "deploy.domain": name }, async () => {
889
+ await withSpan("deploy.dotns", "2. dotns", { "deploy.domain": name, "deploy.subdomain": String(parsed?.isSubdomain ?? false) }, async () => {
848
890
  const dotns = new DotNS();
849
891
  await dotns.connect(
850
892
  options.signer && options.signerAddress ? { signer: options.signer, signerAddress: options.signerAddress } : options.mnemonic ? { mnemonic: options.mnemonic, derivationPath: options.derivationPath } : {}
851
893
  );
852
- const { owned } = await dotns.checkOwnership(name);
853
- if (owned) {
854
- console.log(` Status: Already owned`);
894
+ if (parsed?.isSubdomain) {
895
+ const { owned, owner } = await dotns.checkSubdomainOwnership(parsed.sublabel, parsed.parentLabel);
896
+ if (owned) {
897
+ console.log(` Status: Already owned`);
898
+ } else if (owner) {
899
+ throw new Error(`Subdomain ${parsed.fullName} is owned by ${owner}, not ${dotns.evmAddress}`);
900
+ } else {
901
+ const parentOwnership = await dotns.checkOwnership(parsed.parentLabel);
902
+ if (!parentOwnership.owned) throw new Error(`You must own ${parsed.parentLabel}.dot to register subdomains under it`);
903
+ console.log(` Status: Registering subdomain...`);
904
+ await dotns.registerSubdomain(parsed.sublabel, parsed.parentLabel);
905
+ }
855
906
  } else {
856
- console.log(` Status: Registering...`);
857
- await dotns.register(name);
907
+ const { owned } = await dotns.checkOwnership(name);
908
+ if (owned) {
909
+ console.log(` Status: Already owned`);
910
+ } else {
911
+ console.log(` Status: Registering...`);
912
+ await dotns.register(name);
913
+ }
858
914
  }
859
915
  const contenthashHex = `0x${encodeContenthash(cid)}`;
860
916
  await dotns.setContenthash(name, contenthashHex);
@@ -908,6 +964,7 @@ async function deploy(content, domainName = null, options = {}) {
908
964
  console.log("\n" + "=".repeat(60) + "\n");
909
965
  return { domainName: name, fullDomain: `${name}.dot`, cid, ipfsCid };
910
966
  } finally {
967
+ if (_deployRpcFailedOver) setDeployAttribute("deploy.rpc.failed_over", "true");
911
968
  provider?.client.destroy();
912
969
  }
913
970
  });
@@ -919,6 +976,7 @@ export {
919
976
  friendlyChainError,
920
977
  DEFAULT_BULLETIN_RPC,
921
978
  DEFAULT_POOL_SIZE,
979
+ CHUNK_MORTALITY_PERIOD,
922
980
  isConnectionError,
923
981
  deriveRootSigner,
924
982
  createCID,
@@ -0,0 +1,155 @@
1
+ // src/run-state.ts
2
+ import * as fs from "fs";
3
+ import * as os from "os";
4
+ import * as path from "path";
5
+
6
+ // package.json
7
+ var package_default = {
8
+ name: "bulletin-deploy",
9
+ version: "0.7.4",
10
+ private: false,
11
+ repository: {
12
+ type: "git",
13
+ url: "https://github.com/paritytech/bulletin-deploy.git"
14
+ },
15
+ publishConfig: {
16
+ registry: "https://registry.npmjs.org",
17
+ access: "public"
18
+ },
19
+ type: "module",
20
+ main: "./dist/index.js",
21
+ types: "./dist/index.d.ts",
22
+ bin: {
23
+ "bulletin-deploy": "./bin/bulletin-deploy"
24
+ },
25
+ exports: {
26
+ ".": {
27
+ types: "./dist/index.d.ts",
28
+ import: "./dist/index.js"
29
+ }
30
+ },
31
+ files: [
32
+ "dist",
33
+ "bin"
34
+ ],
35
+ scripts: {
36
+ build: "tsup src/index.ts src/deploy.ts src/dotns.ts src/pool.ts src/telemetry.ts src/memory-report.ts src/merkle.ts src/gh-pages-mirror.ts src/version-check.ts src/bug-report.ts src/run-state.ts --format esm --dts --clean --target node22",
37
+ prepare: "npm run build",
38
+ test: "npm run build && node --test test/test.js test/pool.test.js test/helpers/e2e-helpers.test.js",
39
+ "test:e2e": "npm run build && node --test test/e2e.test.js",
40
+ "test:e2e:smoke": "bash scripts/e2e-pass.sh smoke",
41
+ "test:e2e:pr": "bash scripts/e2e-pass.sh pr",
42
+ "test:e2e:nightly": "bash scripts/e2e-pass.sh nightly",
43
+ benchmark: "npm run build && node benchmark.js"
44
+ },
45
+ dependencies: {
46
+ "@ipld/car": "^5.4.3",
47
+ "@ipld/dag-pb": "^4.1.3",
48
+ "@noble/hashes": "^1.7.2",
49
+ "@parity/dotns-cli": "0.5.6",
50
+ "@polkadot-api/substrate-bindings": "^0.16.5",
51
+ "@polkadot-labs/hdkd": "^0.0.25",
52
+ "@polkadot-labs/hdkd-helpers": "^0.0.26",
53
+ "@polkadot/keyring": "^13.0.0",
54
+ "@polkadot/util-crypto": "^13.0.0",
55
+ "@sentry/node": "^9.14.0",
56
+ "ipfs-unixfs": "^11.2.0",
57
+ "ipfs-unixfs-importer": "^16.1.4",
58
+ multiformats: "^13.4.1",
59
+ "polkadot-api": "^1.23.1",
60
+ viem: "^2.30.5"
61
+ },
62
+ devDependencies: {
63
+ "@types/node": "^22.0.0",
64
+ tsup: "^8.5.0",
65
+ typescript: "^5.9.3"
66
+ },
67
+ minimumVersion: "0.5.6",
68
+ engines: {
69
+ node: ">=22"
70
+ }
71
+ };
72
+
73
+ // src/run-state.ts
74
+ var VERSION = package_default.version;
75
+ function resolveStateDir() {
76
+ if (process.platform === "darwin") {
77
+ return path.join(os.homedir(), "Library", "Application Support", "bulletin-deploy");
78
+ }
79
+ if (process.platform === "win32") {
80
+ const base2 = process.env.LOCALAPPDATA ?? path.join(os.homedir(), "AppData", "Local");
81
+ return path.join(base2, "bulletin-deploy");
82
+ }
83
+ const base = process.env.XDG_STATE_HOME && process.env.XDG_STATE_HOME.length > 0 ? process.env.XDG_STATE_HOME : path.join(os.homedir(), ".local", "state");
84
+ return path.join(base, "bulletin-deploy");
85
+ }
86
+ function stateFilePath() {
87
+ return path.join(resolveStateDir(), "last-run.json");
88
+ }
89
+ function loadRunState() {
90
+ try {
91
+ const raw = fs.readFileSync(stateFilePath(), "utf-8");
92
+ const parsed = JSON.parse(raw);
93
+ if (!parsed || typeof parsed !== "object") return null;
94
+ return parsed;
95
+ } catch {
96
+ return null;
97
+ }
98
+ }
99
+ function writeRunState(patch) {
100
+ try {
101
+ const dir = resolveStateDir();
102
+ fs.mkdirSync(dir, { recursive: true });
103
+ const file = stateFilePath();
104
+ let existing = {};
105
+ try {
106
+ const raw = fs.readFileSync(file, "utf-8");
107
+ const parsed = JSON.parse(raw);
108
+ if (parsed && typeof parsed === "object") existing = parsed;
109
+ } catch {
110
+ }
111
+ const merged = { ...existing, ...patch };
112
+ const tmp = `${file}.${process.pid}.tmp`;
113
+ fs.writeFileSync(tmp, JSON.stringify(merged), { encoding: "utf-8" });
114
+ fs.renameSync(tmp, file);
115
+ } catch {
116
+ }
117
+ }
118
+ function isPidAlive(pid) {
119
+ try {
120
+ process.kill(pid, 0);
121
+ return true;
122
+ } catch (err) {
123
+ const code = err.code;
124
+ if (code === "EPERM") return true;
125
+ return false;
126
+ }
127
+ }
128
+ function shouldSkipStaleWarning(prev) {
129
+ if (prev.pid && isPidAlive(prev.pid)) return true;
130
+ if (prev.toolVersion !== VERSION) return true;
131
+ return false;
132
+ }
133
+ function probablyOomRssMb(override) {
134
+ if (typeof override === "number" && Number.isFinite(override)) return override;
135
+ const env = process.env.BULLETIN_DEPLOY_OOM_HINT_RSS_MB;
136
+ const parsed = env != null ? Number(env) : NaN;
137
+ if (Number.isFinite(parsed) && parsed > 0) return parsed;
138
+ return 1800;
139
+ }
140
+ function shouldShowOomHint(prev) {
141
+ if (prev.lastPeakRssMb == null) return false;
142
+ return prev.lastPeakRssMb >= probablyOomRssMb();
143
+ }
144
+
145
+ export {
146
+ package_default,
147
+ VERSION,
148
+ resolveStateDir,
149
+ stateFilePath,
150
+ loadRunState,
151
+ writeRunState,
152
+ shouldSkipStaleWarning,
153
+ probablyOomRssMb,
154
+ shouldShowOomHint
155
+ };