@parity/product-deploy 0.7.28-rc.0

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 (109) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +233 -0
  3. package/assets/environments.json +313 -0
  4. package/bin/bulletin-bootstrap +84 -0
  5. package/bin/bulletin-deploy +429 -0
  6. package/dist/bug-report.d.ts +29 -0
  7. package/dist/bug-report.js +27 -0
  8. package/dist/chunk-2VAUMZB2.js +284 -0
  9. package/dist/chunk-43HLT335.js +232 -0
  10. package/dist/chunk-5VZQ2KSU.js +231 -0
  11. package/dist/chunk-ADNBLFDP.js +225 -0
  12. package/dist/chunk-BMAEWZYV.js +24 -0
  13. package/dist/chunk-C2TS5MER.js +64 -0
  14. package/dist/chunk-DNXH4QTI.js +2336 -0
  15. package/dist/chunk-FZWJV5AD.js +231 -0
  16. package/dist/chunk-GZD2UFLR.js +8 -0
  17. package/dist/chunk-HOTQDYHD.js +219 -0
  18. package/dist/chunk-IDYGYIMH.js +207 -0
  19. package/dist/chunk-KHVTYIIX.js +146 -0
  20. package/dist/chunk-KJH2T5TQ.js +172 -0
  21. package/dist/chunk-KOSF5FDO.js +49 -0
  22. package/dist/chunk-LZJMVPYW.js +156 -0
  23. package/dist/chunk-MFTODIIT.js +725 -0
  24. package/dist/chunk-MMAZFJDG.js +91 -0
  25. package/dist/chunk-NF2FL4ZO.js +164 -0
  26. package/dist/chunk-OITUIM2E.js +524 -0
  27. package/dist/chunk-P6CHOMN3.js +2368 -0
  28. package/dist/chunk-QMYW3D6E.js +316 -0
  29. package/dist/chunk-QTZNULSH.js +185 -0
  30. package/dist/chunk-RI3ZLNPN.js +71 -0
  31. package/dist/chunk-S7EM5VMW.js +108 -0
  32. package/dist/chunk-T7EEVWNU.js +32 -0
  33. package/dist/chunk-UPWEOGLQ.js +37 -0
  34. package/dist/chunk-ZOC4GITL.js +13 -0
  35. package/dist/chunk-ZYVGHDMU.js +117 -0
  36. package/dist/chunk-probe.d.ts +37 -0
  37. package/dist/chunk-probe.js +18 -0
  38. package/dist/chunker.d.ts +8 -0
  39. package/dist/chunker.js +10 -0
  40. package/dist/deploy.d.ts +299 -0
  41. package/dist/deploy.js +96 -0
  42. package/dist/dotns.d.ts +506 -0
  43. package/dist/dotns.js +101 -0
  44. package/dist/environments.d.ts +104 -0
  45. package/dist/environments.js +23 -0
  46. package/dist/errors.d.ts +6 -0
  47. package/dist/errors.js +8 -0
  48. package/dist/gh-pages-mirror.d.ts +76 -0
  49. package/dist/gh-pages-mirror.js +30 -0
  50. package/dist/incremental-stats.d.ts +69 -0
  51. package/dist/incremental-stats.js +10 -0
  52. package/dist/index.d.ts +23 -0
  53. package/dist/index.js +146 -0
  54. package/dist/manifest/byte-budget.d.ts +46 -0
  55. package/dist/manifest/byte-budget.js +14 -0
  56. package/dist/manifest/config-load.d.ts +36 -0
  57. package/dist/manifest/config-load.js +10 -0
  58. package/dist/manifest/publish.d.ts +54 -0
  59. package/dist/manifest/publish.js +23 -0
  60. package/dist/manifest/schema.d.ts +29 -0
  61. package/dist/manifest/schema.js +10 -0
  62. package/dist/manifest/types.d.ts +90 -0
  63. package/dist/manifest/types.js +6 -0
  64. package/dist/manifest-embed.d.ts +18 -0
  65. package/dist/manifest-embed.js +9 -0
  66. package/dist/manifest-fetch.d.ts +32 -0
  67. package/dist/manifest-fetch.js +21 -0
  68. package/dist/manifest-roundtrip.d.ts +15 -0
  69. package/dist/manifest-roundtrip.js +55 -0
  70. package/dist/manifest.d.ts +44 -0
  71. package/dist/manifest.js +20 -0
  72. package/dist/memory-report.d.ts +95 -0
  73. package/dist/memory-report.js +17 -0
  74. package/dist/merkle.d.ts +50 -0
  75. package/dist/merkle.js +33 -0
  76. package/dist/personhood/bind-paid-alias.d.ts +43 -0
  77. package/dist/personhood/bind-paid-alias.js +10 -0
  78. package/dist/personhood/bind-personal-id.d.ts +55 -0
  79. package/dist/personhood/bind-personal-id.js +12 -0
  80. package/dist/personhood/bootstrap.d.ts +85 -0
  81. package/dist/personhood/bootstrap.js +245 -0
  82. package/dist/personhood/claim-pgas.d.ts +61 -0
  83. package/dist/personhood/claim-pgas.js +12 -0
  84. package/dist/personhood/constants.d.ts +23 -0
  85. package/dist/personhood/constants.js +22 -0
  86. package/dist/personhood/encoding.d.ts +49 -0
  87. package/dist/personhood/encoding.js +24 -0
  88. package/dist/personhood/hashing.d.ts +4 -0
  89. package/dist/personhood/hashing.js +8 -0
  90. package/dist/personhood/member-key.d.ts +12 -0
  91. package/dist/personhood/member-key.js +10 -0
  92. package/dist/personhood/people-client.d.ts +14 -0
  93. package/dist/personhood/people-client.js +48 -0
  94. package/dist/personhood/reprove.d.ts +43 -0
  95. package/dist/personhood/reprove.js +225 -0
  96. package/dist/pool.d.ts +51 -0
  97. package/dist/pool.js +30 -0
  98. package/dist/run-state.d.ts +22 -0
  99. package/dist/run-state.js +20 -0
  100. package/dist/telemetry.d.ts +56 -0
  101. package/dist/telemetry.js +71 -0
  102. package/dist/version-check.d.ts +38 -0
  103. package/dist/version-check.js +30 -0
  104. package/docs/bootstrap.md +49 -0
  105. package/docs/e2e-bootstrap.md +154 -0
  106. package/docs/telemetry.md +62 -0
  107. package/docs/testing.md +44 -0
  108. package/package.json +82 -0
  109. package/tools/release-retry-wrapper.mjs +74 -0
@@ -0,0 +1,2368 @@
1
+ import {
2
+ computeStats,
3
+ renderSummary,
4
+ telemetryAttributes
5
+ } from "./chunk-KHVTYIIX.js";
6
+ import {
7
+ finaliseEmbeddedManifest,
8
+ writeEmbeddedManifestPlaceholder
9
+ } from "./chunk-KOSF5FDO.js";
10
+ import {
11
+ extractManifestFromCar,
12
+ fetchPreviousManifest,
13
+ writePersistentLocalManifest
14
+ } from "./chunk-FZWJV5AD.js";
15
+ import {
16
+ MANIFEST_PATH,
17
+ MANIFEST_VERSION,
18
+ classifyFile,
19
+ parseManifest
20
+ } from "./chunk-S7EM5VMW.js";
21
+ import {
22
+ setDeployContext
23
+ } from "./chunk-ADNBLFDP.js";
24
+ import {
25
+ probeChunks
26
+ } from "./chunk-QTZNULSH.js";
27
+ import {
28
+ packSection
29
+ } from "./chunk-C2TS5MER.js";
30
+ import {
31
+ DotNS,
32
+ PublisherNotSupportedError,
33
+ TX_TIMEOUT_MS,
34
+ fetchNonce,
35
+ parseDomainName,
36
+ popStatusName,
37
+ verifyNonceAdvanced
38
+ } from "./chunk-DNXH4QTI.js";
39
+ import {
40
+ derivePoolAccounts,
41
+ detectTestnet,
42
+ ensureAuthorized,
43
+ fetchPoolAuthorizations,
44
+ selectAccount,
45
+ topUpBy
46
+ } from "./chunk-QMYW3D6E.js";
47
+ import {
48
+ VERSION,
49
+ captureWarning,
50
+ initTelemetry,
51
+ resolveRunner,
52
+ resolveRunnerType,
53
+ sampleMemory,
54
+ setDeployAttribute,
55
+ setDeployReportContext,
56
+ setDeploySentryTag,
57
+ truncateAddress,
58
+ withDeploySpan,
59
+ withSpan
60
+ } from "./chunk-MFTODIIT.js";
61
+ import {
62
+ DEFAULT_ENV_ID,
63
+ getPopSelfServeConfig,
64
+ loadEnvironments,
65
+ resolveEndpoints
66
+ } from "./chunk-OITUIM2E.js";
67
+ import {
68
+ NonRetryableError
69
+ } from "./chunk-ZOC4GITL.js";
70
+ import {
71
+ MirrorSkipped,
72
+ mirrorToGitHubPages,
73
+ pollMirrorFreshness
74
+ } from "./chunk-HOTQDYHD.js";
75
+
76
+ // src/merkle.ts
77
+ import * as fs2 from "fs";
78
+ import * as path2 from "path";
79
+ import { execSync as execSync2 } from "child_process";
80
+ import { importer } from "ipfs-unixfs-importer";
81
+ import { CarReader as CarReader2 } from "@ipld/car/reader";
82
+ import { CarWriter } from "@ipld/car/writer";
83
+ import { CID as CID2 } from "multiformats/cid";
84
+ import * as dagPB2 from "@ipld/dag-pb";
85
+ import { UnixFS as UnixFS2 } from "ipfs-unixfs";
86
+
87
+ // src/deploy.ts
88
+ import { Buffer } from "buffer";
89
+ import * as fs from "fs";
90
+ import * as path from "path";
91
+ import { execSync } from "child_process";
92
+ import { sha256 } from "@noble/hashes/sha256";
93
+ import { blake2b } from "@noble/hashes/blake2b";
94
+ import { createClient as createPolkadotClient, Enum } from "polkadot-api";
95
+ import { getWsProvider, WsEvent } from "polkadot-api/ws";
96
+ import { CID } from "multiformats/cid";
97
+ import { create as createMultihash } from "multiformats/hashes/digest";
98
+ import { base32 } from "multiformats/bases/base32";
99
+ import { base58btc } from "multiformats/bases/base58";
100
+ import * as dagPB from "@ipld/dag-pb";
101
+ import { UnixFS } from "ipfs-unixfs";
102
+ import { cryptoWaitReady } from "@polkadot/util-crypto";
103
+ import { getPolkadotSigner } from "polkadot-api/signer";
104
+ import { sr25519CreateDerive } from "@polkadot-labs/hdkd";
105
+ import { mnemonicToEntropy, entropyToMiniSecret, ss58Address } from "@polkadot-labs/hdkd-helpers";
106
+ import { CarReader } from "@ipld/car/reader";
107
+ function friendlyChainError(msg) {
108
+ if (/"type":\s*"Invalid"[\s\S]*?"type":\s*"Payment"/i.test(msg)) {
109
+ return "Bulletin quota exhausted (signed extension rejected the tx \u2014 signer is out of allowed txs or bytes; grant quota on-chain)";
110
+ }
111
+ return msg;
112
+ }
113
+ var DEFAULT_BULLETIN_RPC = "wss://paseo-bulletin-rpc.polkadot.io";
114
+ var DEFAULT_POOL_SIZE = 10;
115
+ var BULLETIN_ENDPOINTS = [DEFAULT_BULLETIN_RPC];
116
+ var POOL_SIZE = DEFAULT_POOL_SIZE;
117
+ var _deployRpcFailedOver = false;
118
+ var _onWsHalt = null;
119
+ function setWsHaltCallback(cb) {
120
+ _onWsHalt = cb;
121
+ }
122
+ function makeBulletinStatusHandler(primary) {
123
+ return (s) => {
124
+ if (s.type === WsEvent.CONNECTED && s.uri !== primary) {
125
+ _deployRpcFailedOver = true;
126
+ setDeployAttribute("deploy.rpc.failed_over", "true");
127
+ captureWarning("Bulletin RPC failover", { from: primary, to: s.uri });
128
+ }
129
+ if (s.type === WsEvent.CLOSE || s.type === WsEvent.ERROR) {
130
+ try {
131
+ _onWsHalt?.();
132
+ } catch {
133
+ }
134
+ }
135
+ };
136
+ }
137
+ var CHUNK_SIZE = 2 * 1024 * 1024;
138
+ var MAX_FILE_SIZE = 8 * 1024 * 1024;
139
+ var MAX_RECONNECTIONS = parseInt(process.env.BULLETIN_MAX_RECONNECTIONS ?? "3", 10);
140
+ var CHUNK_TIMEOUT_MS = parseInt(process.env.BULLETIN_CHUNK_TIMEOUT_MS ?? "180000", 10);
141
+ var CHUNK_MORTALITY_PERIOD = (() => {
142
+ const v = parseInt(process.env.BULLETIN_CHUNK_MORTALITY_PERIOD ?? "", 10);
143
+ return Number.isFinite(v) && v > 0 ? v : 16;
144
+ })();
145
+ var RETRY_BASE_DELAY_MS = 2e3;
146
+ var RETRY_MAX_DELAY_MS = 15e3;
147
+ var WS_HEARTBEAT_TIMEOUT_MS = 3e5;
148
+ var GRANDPA_NATURAL_WAIT_MS = parseInt(process.env.BULLETIN_GRANDPA_NATURAL_WAIT_MS ?? "90000", 10);
149
+ var GRANDPA_REUPLOAD_POLL_MS = 5e3;
150
+ var GRANDPA_REUPLOAD_TIMEOUT_MS = 12e4;
151
+ var GRANDPA_REUPLOAD_MAX_ROUNDS = 3;
152
+ var RETRY_BUDGET_MAX_EVENTS = parseInt(process.env.BULLETIN_RETRY_BUDGET_MAX ?? "5", 10);
153
+ var RETRY_BUDGET_WINDOW_MS = parseInt(process.env.BULLETIN_RETRY_BUDGET_WINDOW_MS ?? "30000", 10);
154
+ function retryBudgetExhausted(history, maxEvents, windowMs, now = Date.now()) {
155
+ let inWindow = 0;
156
+ for (const t of history) {
157
+ if (now - t <= windowMs) inWindow++;
158
+ }
159
+ return inWindow > maxEvents;
160
+ }
161
+ function isConnectionError(error) {
162
+ const msg = error?.message || String(error);
163
+ return /heartbeat timeout|WS halt|Unable to connect|ChainHead disjointed/i.test(msg);
164
+ }
165
+ var CID_CONFIG = { version: 1, codec: 85, hashCode: 18, hashLength: 32 };
166
+ function deriveRootSigner(mnemonic, path3 = "") {
167
+ const entropy = mnemonicToEntropy(mnemonic);
168
+ const miniSecret = entropyToMiniSecret(entropy);
169
+ const derive = sr25519CreateDerive(miniSecret);
170
+ const keyPair = derive(path3);
171
+ const signer = getPolkadotSigner(keyPair.publicKey, "Sr25519", keyPair.sign);
172
+ return { signer, ss58: ss58Address(keyPair.publicKey) };
173
+ }
174
+ function createCID(data, codec = CID_CONFIG.codec, hashCode = CID_CONFIG.hashCode) {
175
+ let hash;
176
+ if (hashCode === 45600) hash = blake2b(data, { dkLen: CID_CONFIG.hashLength });
177
+ else if (hashCode === 18) hash = sha256(data);
178
+ else throw new Error(`Unsupported hash code: 0x${hashCode.toString(16)}`);
179
+ return CID.createV1(codec, createMultihash(hashCode, hash));
180
+ }
181
+ function encodeContenthash(cidString) {
182
+ const decoder = cidString.startsWith("Qm") ? base58btc : base32;
183
+ const cid = CID.parse(cidString, decoder);
184
+ const contenthash = new Uint8Array(cid.bytes.length + 2);
185
+ contenthash[0] = 227;
186
+ contenthash[1] = 1;
187
+ contenthash.set(cid.bytes, 2);
188
+ return Buffer.from(contenthash).toString("hex");
189
+ }
190
+ var ENCRYPT_MAGIC = new Uint8Array([68, 79, 84, 76, 73, 95, 69, 78, 67, 1]);
191
+ var ENCRYPT_SALT_LEN = 16;
192
+ var ENCRYPT_NONCE_LEN = 12;
193
+ var ENCRYPT_TAG_LEN = 16;
194
+ var ENCRYPT_KEY_LEN = 32;
195
+ var ENCRYPT_PBKDF2_ITERATIONS = 1e5;
196
+ async function encryptContent(data, password) {
197
+ const { webcrypto, createCipheriv } = await import("crypto");
198
+ const subtle = webcrypto.subtle;
199
+ const salt = webcrypto.getRandomValues(new Uint8Array(ENCRYPT_SALT_LEN));
200
+ const nonce = webcrypto.getRandomValues(new Uint8Array(ENCRYPT_NONCE_LEN));
201
+ const keyMaterial = await subtle.importKey("raw", Buffer.from(password, "utf-8"), "PBKDF2", false, ["deriveBits"]);
202
+ const keyBits = await subtle.deriveBits(
203
+ { name: "PBKDF2", salt, iterations: ENCRYPT_PBKDF2_ITERATIONS, hash: "SHA-256" },
204
+ keyMaterial,
205
+ ENCRYPT_KEY_LEN * 8
206
+ );
207
+ const cipher = createCipheriv("chacha20-poly1305", Buffer.from(keyBits), nonce, { authTagLength: ENCRYPT_TAG_LEN });
208
+ cipher.setAAD(ENCRYPT_MAGIC, { plaintextLength: data.length });
209
+ return Buffer.concat([ENCRYPT_MAGIC, salt, nonce, cipher.update(data), cipher.final(), cipher.getAuthTag()]);
210
+ }
211
+ function toHashingEnum(mhCode) {
212
+ switch (mhCode) {
213
+ case 45600:
214
+ return { type: "Blake2b256", value: void 0 };
215
+ case 18:
216
+ return { type: "Sha2_256", value: void 0 };
217
+ case 27:
218
+ return { type: "Keccak256", value: void 0 };
219
+ default:
220
+ throw new Error(`Unhandled multihash code: ${mhCode}`);
221
+ }
222
+ }
223
+ async function getProvider() {
224
+ const primary = BULLETIN_ENDPOINTS[0];
225
+ console.log(` Connecting to Bulletin: ${primary}`);
226
+ const client = createPolkadotClient(getWsProvider(
227
+ BULLETIN_ENDPOINTS,
228
+ { heartbeatTimeout: WS_HEARTBEAT_TIMEOUT_MS, onStatusChanged: makeBulletinStatusHandler(primary) }
229
+ ));
230
+ const unsafeApi = client.getUnsafeApi();
231
+ try {
232
+ await cryptoWaitReady();
233
+ const poolMnemonic = process.env.BULLETIN_POOL_MNEMONIC || void 0;
234
+ const poolAccounts = derivePoolAccounts(POOL_SIZE, poolMnemonic);
235
+ const [authorizations, currentBlockForPool] = await Promise.all([
236
+ fetchPoolAuthorizations(unsafeApi, poolAccounts),
237
+ unsafeApi.query.System.Number.getValue()
238
+ ]);
239
+ const selectionResult = selectAccount(authorizations, Math.random, currentBlockForPool);
240
+ let selectedAccount;
241
+ let eligibleCount = 0;
242
+ if (!selectionResult) {
243
+ const best = authorizations.reduce((a, b) => a.transactions > b.transactions ? a : b);
244
+ console.log(` All pool accounts low on capacity, auto-authorizing account ${best.index}...`);
245
+ await ensureAuthorized(unsafeApi, best.address, `pool account ${best.index}`);
246
+ selectedAccount = best;
247
+ } else {
248
+ selectedAccount = selectionResult.account;
249
+ eligibleCount = selectionResult.eligibleCount;
250
+ await ensureAuthorized(unsafeApi, selectedAccount.address, `pool account ${selectedAccount.index}`);
251
+ }
252
+ console.log(` Using pool account ${selectedAccount.index}: ${selectedAccount.address}`);
253
+ setDeployAttribute("deploy.signer.mode", "pool");
254
+ setDeployAttribute("deploy.pool.account", truncateAddress(selectedAccount.address));
255
+ setDeployAttribute("deploy.pool.index", String(selectedAccount.index));
256
+ setDeployAttribute("deploy.pool.eligible_count", eligibleCount);
257
+ return { client, unsafeApi, signer: selectedAccount.signer, ss58: selectedAccount.address };
258
+ } catch (e) {
259
+ client.destroy();
260
+ throw e;
261
+ }
262
+ }
263
+ async function getDirectProvider(mnemonic, derivationPath = "", bulletinAuthorizeV2) {
264
+ const primary = BULLETIN_ENDPOINTS[0];
265
+ console.log(` Connecting to Bulletin: ${primary}`);
266
+ const client = createPolkadotClient(getWsProvider(
267
+ BULLETIN_ENDPOINTS,
268
+ { heartbeatTimeout: WS_HEARTBEAT_TIMEOUT_MS, onStatusChanged: makeBulletinStatusHandler(primary) }
269
+ ));
270
+ const unsafeApi = client.getUnsafeApi();
271
+ const { signer, ss58 } = deriveRootSigner(mnemonic, derivationPath);
272
+ console.log(` Using direct signer: ${ss58}${derivationPath ? ` (path: ${derivationPath})` : ""}`);
273
+ let [auth, currentBlock] = await Promise.all([
274
+ unsafeApi.query.TransactionStorage.Authorizations.getValue(Enum("Account", ss58)),
275
+ client.getFinalizedBlock()
276
+ ]);
277
+ let now = currentBlock.number;
278
+ if (!auth || Number(auth.expiration ?? 0) <= now) {
279
+ try {
280
+ await ensureAuthorized(unsafeApi, ss58, "direct signer", bulletinAuthorizeV2);
281
+ [auth, currentBlock] = await Promise.all([
282
+ unsafeApi.query.TransactionStorage.Authorizations.getValue(Enum("Account", ss58)),
283
+ client.getFinalizedBlock()
284
+ ]);
285
+ now = currentBlock.number;
286
+ } catch (e) {
287
+ client.destroy();
288
+ throw new NonRetryableError(`Account ${ss58} is not authorized for Bulletin storage and auto-authorization failed: ${e.message}`);
289
+ }
290
+ }
291
+ console.log(` Authorization: expires at block ${Number(auth?.expiration ?? 0)} (current: ${now})`);
292
+ setDeployAttribute("deploy.signer.mode", "direct");
293
+ setDeployAttribute("deploy.signer.address", truncateAddress(ss58));
294
+ return { client, unsafeApi, signer, ss58 };
295
+ }
296
+ function watchTransaction(tx, signer, txOpts, onSuccess, { label = "transaction", rpc, senderSS58, expectedNonce, timeoutMs, fetchNonce: fetchNonceOverride } = {}) {
297
+ const timeout = timeoutMs ?? TX_TIMEOUT_MS;
298
+ const _fetchNonce = fetchNonceOverride ?? fetchNonce;
299
+ return new Promise((resolve2, reject) => {
300
+ let settled = false;
301
+ let sub;
302
+ const settle = (fn) => (...args) => {
303
+ if (settled) return;
304
+ settled = true;
305
+ clearTimeout(timer);
306
+ try {
307
+ sub?.unsubscribe();
308
+ } catch {
309
+ }
310
+ fn(...args);
311
+ };
312
+ const tryNonceFallback = async () => {
313
+ if (!rpc || !senderSS58 || expectedNonce == null) return false;
314
+ try {
315
+ const endpoints = Array.isArray(rpc) ? rpc : [rpc];
316
+ const verified = await verifyNonceAdvanced(endpoints, senderSS58, expectedNonce);
317
+ if (settled) return true;
318
+ if (verified.advanced) {
319
+ console.log(` ${label}: nonce advanced past ${expectedNonce} (witnessed by ${verified.witnessRpc}), tx was included`);
320
+ settle(resolve2)({ value: onSuccess(), viaFallback: true });
321
+ return true;
322
+ }
323
+ } catch (e) {
324
+ if (settled) return true;
325
+ console.log(` ${label}: nonce fallback failed: ${e.message?.slice(0, 80)}`);
326
+ }
327
+ return false;
328
+ };
329
+ const timer = setTimeout(async () => {
330
+ if (settled) return;
331
+ if (await tryNonceFallback()) return;
332
+ settle(reject)(new Error(`${label} timed out after ${timeout / 1e3}s waiting for block confirmation`));
333
+ }, timeout);
334
+ sub = tx.signSubmitAndWatch(signer, txOpts).subscribe({
335
+ next: async (event) => {
336
+ if (event.type !== "txBestBlocksState") return;
337
+ if (event.found) {
338
+ if (event.ok) {
339
+ const receipt = event.block ? { txHash: String(event.txHash), blockHash: String(event.block.hash), blockNumber: Number(event.block.number) } : void 0;
340
+ settle(resolve2)({ value: onSuccess(event), viaFallback: false, receipt });
341
+ } else settle(reject)(new Error(`${label} dispatch error`));
342
+ return;
343
+ }
344
+ if (event.isValid === false) {
345
+ console.log(` ${label}: tx rejected by pool (isValid:false), checking nonce fallback...`);
346
+ if (await tryNonceFallback()) return;
347
+ settle(reject)(new Error(`${label} tx rejected by pool (isValid:false)`));
348
+ }
349
+ },
350
+ error: (e) => {
351
+ const msg = e?.message || String(e).slice(0, 500);
352
+ settle(reject)(new Error(`${label} subscription error: ${friendlyChainError(msg)}`));
353
+ }
354
+ });
355
+ });
356
+ }
357
+ async function storeChunk(unsafeApi, signer, chunkBytes, nonce, ss58, opts = {}) {
358
+ const hashCode = 18;
359
+ const cid = createCID(chunkBytes, CID_CONFIG.codec, hashCode);
360
+ const tx = unsafeApi.tx.TransactionStorage.store_with_cid_config({ cid: { codec: BigInt(CID_CONFIG.codec), hashing: toHashingEnum(hashCode) }, data: chunkBytes });
361
+ const txOpts = { mortality: { mortal: true, period: CHUNK_MORTALITY_PERIOD }, nonce };
362
+ const { value, viaFallback, receipt } = await watchTransaction(tx, signer, txOpts, () => {
363
+ console.log(` CID: ${cid.toString()}`);
364
+ return { cid, len: chunkBytes.length };
365
+ }, { label: `chunk(nonce:${nonce})`, rpc: BULLETIN_ENDPOINTS, senderSS58: ss58, expectedNonce: nonce, timeoutMs: CHUNK_TIMEOUT_MS, fetchNonce: opts.fetchNonce });
366
+ return { ...value, viaFallback, receipt };
367
+ }
368
+ async function storeFile(contentBytes, { client: existingClient, unsafeApi: existingApi, signer: existingSigner } = {}) {
369
+ console.log(`
370
+ Size: ${(contentBytes.length / 1024).toFixed(2)} KB`);
371
+ if (contentBytes.length > MAX_FILE_SIZE) throw new Error(`File exceeds 8MB limit. Use chunked deployment.`);
372
+ const hashCode = 18;
373
+ const cid = createCID(contentBytes, CID_CONFIG.codec, hashCode);
374
+ console.log(` CID: ${cid.toString()}`);
375
+ let client, unsafeApi, signer;
376
+ if (existingClient) {
377
+ client = existingClient;
378
+ unsafeApi = existingApi;
379
+ signer = existingSigner;
380
+ } else {
381
+ const provider = await getProvider();
382
+ client = provider.client;
383
+ unsafeApi = provider.unsafeApi;
384
+ signer = provider.signer;
385
+ }
386
+ try {
387
+ const tx = unsafeApi.tx.TransactionStorage.store_with_cid_config({ cid: { codec: BigInt(CID_CONFIG.codec), hashing: toHashingEnum(hashCode) }, data: contentBytes });
388
+ const txOpts = { mortality: { mortal: true, period: 256 } };
389
+ console.log(` Submitting...`);
390
+ const { value } = await watchTransaction(tx, signer, txOpts, (event) => {
391
+ console.log(` Block: ${event?.block?.hash ?? "confirmed"}
392
+ `);
393
+ return cid.toString();
394
+ }, { label: "storeFile" });
395
+ if (!existingClient) client.destroy();
396
+ return value;
397
+ } catch (e) {
398
+ if (!existingClient) client.destroy();
399
+ throw e;
400
+ }
401
+ }
402
+ function assignDenseNonces(stored, startNonce) {
403
+ const nonces = /* @__PURE__ */ new Map();
404
+ let counter = 0;
405
+ for (let i = 0; i < stored.length; i++) {
406
+ if (stored[i] === null) {
407
+ nonces.set(i, startNonce + counter);
408
+ counter++;
409
+ }
410
+ }
411
+ return nonces;
412
+ }
413
+ var __assignDenseNoncesForTest = assignDenseNonces;
414
+ async function storeChunkedContent(chunks, { client: existingClient, unsafeApi: existingApi, signer: existingSigner, ss58: existingSS58, reconnect, fetchNonce: fetchNonceOverride, skipCids, probeFailedCids, gateway: providerGateway, bulletinAuthorizeV2, trustedCids, skipRootStore } = {}) {
415
+ const _fetchNonce = fetchNonceOverride ?? fetchNonce;
416
+ console.log(`
417
+ Chunks: ${chunks.length}`);
418
+ const totalBytes = chunks.reduce((s, c) => s + c.length, 0);
419
+ console.log(` Total: ${(totalBytes / 1024).toFixed(2)} KB`);
420
+ let client, unsafeApi, signer, ss58;
421
+ let ownsClient = false;
422
+ if (existingClient) {
423
+ client = existingClient;
424
+ unsafeApi = existingApi;
425
+ signer = existingSigner;
426
+ ss58 = existingSS58;
427
+ } else {
428
+ const provider = await getProvider();
429
+ client = provider.client;
430
+ unsafeApi = provider.unsafeApi;
431
+ signer = provider.signer;
432
+ ss58 = provider.ss58;
433
+ ownsClient = true;
434
+ }
435
+ const refreshExistingClient = async (reason) => {
436
+ if (!reconnect) return false;
437
+ console.log(`
438
+ Connection lost (${reason}), reconnecting...`);
439
+ const fresh = await reconnect();
440
+ client = fresh.client;
441
+ unsafeApi = fresh.unsafeApi;
442
+ signer = fresh.signer;
443
+ ss58 = fresh.ss58;
444
+ ownsClient = true;
445
+ return true;
446
+ };
447
+ if (existingClient && reconnect) {
448
+ try {
449
+ await unsafeApi.query.System.Number.getValue();
450
+ } catch (e) {
451
+ if (isConnectionError(e)) {
452
+ await refreshExistingClient("stale client detected by pre-upload probe");
453
+ } else {
454
+ throw e;
455
+ }
456
+ }
457
+ }
458
+ const requiredTxs = BigInt(chunks.length + 1);
459
+ const requiredBytes = BigInt(totalBytes);
460
+ const readUploadAuthorization = () => Promise.all([
461
+ unsafeApi.query.TransactionStorage.Authorizations.getValue(Enum("Account", ss58)),
462
+ unsafeApi.query.System.Number.getValue()
463
+ ]);
464
+ let uploadAuth;
465
+ let currentBlockNum;
466
+ try {
467
+ [uploadAuth, currentBlockNum] = await readUploadAuthorization();
468
+ } catch (e) {
469
+ if (existingClient && reconnect && isConnectionError(e)) {
470
+ await refreshExistingClient("authorization preflight hit a stale chainHead");
471
+ [uploadAuth, currentBlockNum] = await readUploadAuthorization();
472
+ } else {
473
+ throw e;
474
+ }
475
+ }
476
+ const txsRemaining = uploadAuth ? BigInt(uploadAuth.extent.transactions_allowance) - BigInt(uploadAuth.extent.transactions) : 0n;
477
+ const bytesRemaining = uploadAuth ? BigInt(uploadAuth.extent.bytes_allowance) - BigInt(uploadAuth.extent.bytes) : 0n;
478
+ const isAuthorized = uploadAuth !== void 0 && Number(uploadAuth.expiration ?? 0) > currentBlockNum;
479
+ if (!isAuthorized || txsRemaining < requiredTxs || bytesRemaining < requiredBytes) {
480
+ const txsRemainingDisplay = txsRemaining < 0n ? 0n : txsRemaining;
481
+ const bytesRemainingDisplay = bytesRemaining < 0n ? 0n : bytesRemaining;
482
+ console.log(`
483
+ Account needs re-authorization (authorized=${isAuthorized}, need ${requiredTxs} txs / ${(totalBytes / 1e6).toFixed(1)}MB, have ${txsRemainingDisplay} txs / ${(Number(bytesRemainingDisplay) / 1e6).toFixed(2)}MB)`);
484
+ console.log(` Attempting to re-authorize with Alice...`);
485
+ try {
486
+ await ensureAuthorized(unsafeApi, ss58, void 0, bulletinAuthorizeV2, { txs: requiredTxs, bytes: requiredBytes });
487
+ console.log(` Re-authorization successful`);
488
+ } catch (e) {
489
+ throw new NonRetryableError(`Account ${ss58} has insufficient Bulletin authorization and auto-authorization via Alice failed (${e.message}). Authorize the account on-chain.`);
490
+ }
491
+ }
492
+ let reconnectionsUsed = 0;
493
+ const recoveryHistory = [];
494
+ const recordRecoveryAndCheckBudget = (kind) => {
495
+ const now = Date.now();
496
+ recoveryHistory.push(now);
497
+ if (retryBudgetExhausted(recoveryHistory, RETRY_BUDGET_MAX_EVENTS, RETRY_BUDGET_WINDOW_MS, now)) {
498
+ captureWarning("Retry budget exhausted", {
499
+ kind,
500
+ events: recoveryHistory.length,
501
+ maxEvents: RETRY_BUDGET_MAX_EVENTS,
502
+ windowMs: RETRY_BUDGET_WINDOW_MS
503
+ });
504
+ throw new Error(
505
+ `Retry budget exhausted: more than ${RETRY_BUDGET_MAX_EVENTS} recovery attempts within ${Math.round(RETRY_BUDGET_WINDOW_MS / 1e3)}s. Chain RPC is unstable; bailing to bound peak memory.`
506
+ );
507
+ }
508
+ };
509
+ async function doReconnect() {
510
+ if (!reconnect || reconnectionsUsed >= MAX_RECONNECTIONS) {
511
+ throw new Error(`Connection lost and max reconnections (${MAX_RECONNECTIONS}) exhausted`);
512
+ }
513
+ recordRecoveryAndCheckBudget("reconnect");
514
+ reconnectionsUsed++;
515
+ setDeployAttribute("deploy.reconnects", reconnectionsUsed);
516
+ const delay = Math.min(RETRY_BASE_DELAY_MS * Math.pow(2, reconnectionsUsed - 1), RETRY_MAX_DELAY_MS);
517
+ console.log(`
518
+ Connection lost, reconnecting to Bulletin in ${(delay / 1e3).toFixed(0)}s (${reconnectionsUsed}/${MAX_RECONNECTIONS})...`);
519
+ captureWarning("WebSocket connection lost, reconnecting", { reconnection: reconnectionsUsed, maxReconnections: MAX_RECONNECTIONS });
520
+ sampleMemory(`reconnect_${reconnectionsUsed}_before`);
521
+ try {
522
+ client.destroy();
523
+ } catch {
524
+ }
525
+ await new Promise((r) => setTimeout(r, delay));
526
+ const fresh = await reconnect();
527
+ client = fresh.client;
528
+ unsafeApi = fresh.unsafeApi;
529
+ signer = fresh.signer;
530
+ ss58 = fresh.ss58;
531
+ wsHaltDetected = false;
532
+ ownsClient = true;
533
+ sampleMemory(`reconnect_${reconnectionsUsed}_after`);
534
+ }
535
+ let wsHaltDetected = false;
536
+ setWsHaltCallback(() => {
537
+ wsHaltDetected = true;
538
+ try {
539
+ client.destroy();
540
+ } catch {
541
+ }
542
+ });
543
+ try {
544
+ let startNonce = await _fetchNonce(BULLETIN_ENDPOINTS, ss58);
545
+ console.log(` Starting nonce: ${startNonce}`);
546
+ const BATCH_SIZE_INITIAL = 2;
547
+ const BATCH_SIZE_RECOVERY = 1;
548
+ const MAX_CHUNK_RETRIES = 3;
549
+ const MAX_REPROBE_RETRIES = 3;
550
+ console.log(`
551
+ Submitting ${chunks.length} chunks in batches of up to ${BATCH_SIZE_INITIAL}...`);
552
+ const stored = new Array(chunks.length).fill(null);
553
+ let tier2Verified = 0;
554
+ let tier2Inconclusive = 0;
555
+ let tier2Fallback = 0;
556
+ const skipProbeResults = /* @__PURE__ */ new Map();
557
+ const chunkCidsComputed = new Array(chunks.length);
558
+ let trustedCount = 0;
559
+ const trustedIndices = [];
560
+ const skipCidsCandidates = [];
561
+ for (let i = 0; i < chunks.length; i++) {
562
+ const cid = createCID(chunks[i], CID_CONFIG.codec, 18);
563
+ chunkCidsComputed[i] = cid;
564
+ const cidStr = cid.toString();
565
+ if (trustedCids?.has(cidStr)) {
566
+ stored[i] = { cid, len: chunks[i].length, viaFallback: true };
567
+ trustedCount++;
568
+ trustedIndices.push(i);
569
+ } else if (skipCids?.has(cidStr)) {
570
+ skipCidsCandidates.push({ index: i, cid });
571
+ }
572
+ }
573
+ if (trustedCount > 0) {
574
+ const indicesStr = trustedIndices.length > 10 ? `${trustedIndices.slice(0, 5).join(", ")}, \u2026, ${trustedIndices.slice(-3).join(", ")}` : trustedIndices.join(", ");
575
+ console.log(` Trusted: ${trustedCount} chunks skipped without re-probe (chunks ${indicesStr})`);
576
+ }
577
+ if (skipCidsCandidates.length > 0) {
578
+ const cidStrings = skipCidsCandidates.map((c) => c.cid.toString());
579
+ const probeResults = await probeChunks(cidStrings, { client });
580
+ const probeResultMap = new Map(probeResults.map((r) => [r.cid, r.present]));
581
+ for (const r of probeResults) skipProbeResults.set(r.cid, r.present);
582
+ let confirmedCount = 0;
583
+ for (const { index: i, cid } of skipCidsCandidates) {
584
+ const cidStr = cid.toString();
585
+ const present = probeResultMap.get(cidStr) ?? null;
586
+ if (present !== false) {
587
+ stored[i] = { cid, len: chunks[i].length, viaFallback: true };
588
+ confirmedCount++;
589
+ if (present === true) {
590
+ tier2Verified++;
591
+ } else {
592
+ tier2Inconclusive++;
593
+ }
594
+ } else {
595
+ tier2Fallback++;
596
+ }
597
+ }
598
+ console.log(` Cache check: ${confirmedCount} confirmed, ${tier2Fallback} missing${tier2Fallback > 0 ? " (will upload)" : ""}`);
599
+ }
600
+ const assignedNonces = assignDenseNonces(stored, startNonce);
601
+ const uploadTotal = stored.filter((s) => s === null).length;
602
+ let uploadEmitted = 0;
603
+ const nonceAdvanceIndices = /* @__PURE__ */ new Set();
604
+ let b = 0;
605
+ while (b < chunks.length) {
606
+ if (wsHaltDetected && reconnect && reconnectionsUsed < MAX_RECONNECTIONS) {
607
+ wsHaltDetected = false;
608
+ await doReconnect();
609
+ }
610
+ const batchSize = reconnectionsUsed > 0 ? BATCH_SIZE_RECOVERY : BATCH_SIZE_INITIAL;
611
+ const batchIndices = [];
612
+ const batchChunks = [];
613
+ for (let j = 0; j < batchSize && b + j < chunks.length; j++) {
614
+ const i = b + j;
615
+ if (stored[i] === null) {
616
+ batchIndices.push(i);
617
+ batchChunks.push(chunks[i]);
618
+ }
619
+ }
620
+ if (batchIndices.length === 0) {
621
+ b += batchSize;
622
+ continue;
623
+ }
624
+ const batchPromises = batchChunks.map((chunkData, j) => {
625
+ const i = batchIndices[j];
626
+ const nonce = assignedNonces.get(i);
627
+ uploadEmitted++;
628
+ console.log(` [${uploadEmitted}/${uploadTotal}] chunk ${i} \u2014 ${(chunkData.length / 1024 / 1024).toFixed(2)} MB (nonce: ${nonce})`);
629
+ return storeChunk(unsafeApi, signer, chunkData, nonce, ss58, { fetchNonce: fetchNonceOverride });
630
+ });
631
+ const results = await Promise.allSettled(batchPromises);
632
+ results.forEach((r, j) => {
633
+ if (r.status === "fulfilled") {
634
+ stored[batchIndices[j]] = r.value;
635
+ assignedNonces.delete(batchIndices[j]);
636
+ if (r.value.viaFallback) nonceAdvanceIndices.add(batchIndices[j]);
637
+ }
638
+ });
639
+ const failures = results.map((r, j) => r.status === "rejected" ? { index: batchIndices[j], chunkData: batchChunks[j], error: r.reason } : null).filter(Boolean);
640
+ const needsReconnect = failures.some((f) => isConnectionError(f.error));
641
+ if (needsReconnect && reconnect && reconnectionsUsed < MAX_RECONNECTIONS) {
642
+ await doReconnect();
643
+ const currentNonce = await _fetchNonce(BULLETIN_ENDPOINTS, ss58);
644
+ for (const idx of batchIndices) {
645
+ const chunkNonce = assignedNonces.get(idx);
646
+ if (chunkNonce !== void 0 && chunkNonce < currentNonce && stored[idx] === null) {
647
+ console.log(` Chunk ${idx + 1}: nonce ${chunkNonce} consumed (current=${currentNonce}), treating as included`);
648
+ stored[idx] = { cid: createCID(chunks[idx], CID_CONFIG.codec, 18), len: chunks[idx].length, viaFallback: true };
649
+ nonceAdvanceIndices.add(idx);
650
+ assignedNonces.delete(idx);
651
+ }
652
+ }
653
+ startNonce = Math.max(startNonce, currentNonce);
654
+ if (failures.some((f) => stored[f.index] === null)) {
655
+ continue;
656
+ }
657
+ }
658
+ for (const fail of failures) {
659
+ if (stored[fail.index] !== null) {
660
+ continue;
661
+ }
662
+ const failCid = createCID(fail.chunkData, CID_CONFIG.codec, 18);
663
+ if (probeFailedCids && probeFailedCids.has(failCid.toString()) && fail.error?.message?.includes("isValid:false")) {
664
+ console.log(` Chunk ${fail.index + 1}: isValid:false but CID was probe-failed \u2014 treating as already on chain`);
665
+ captureWarning("isValid:false treated as success (probe-failed backstop)", { chunkIndex: fail.index + 1, cid: failCid.toString() });
666
+ stored[fail.index] = { cid: failCid, len: fail.chunkData.length, viaFallback: true };
667
+ continue;
668
+ }
669
+ captureWarning("Chunk upload failed, retrying", { chunkIndex: fail.index + 1, maxRetries: MAX_CHUNK_RETRIES, error: fail.error?.message?.slice(0, 200) });
670
+ const isExpiryFailure = fail.error?.message?.includes("isValid:false");
671
+ if (isExpiryFailure) {
672
+ console.log(` Chunk ${fail.index + 1}: tx rejected (isValid:false), likely mortal era expiry \u2014 reissuing with fresh nonce`);
673
+ }
674
+ let retried = false;
675
+ for (let attempt = 1; attempt <= MAX_CHUNK_RETRIES; attempt++) {
676
+ recordRecoveryAndCheckBudget("chunk_retry");
677
+ const retryDelay = Math.min(RETRY_BASE_DELAY_MS * Math.pow(2, attempt - 1), RETRY_MAX_DELAY_MS);
678
+ console.log(` Retrying chunk ${fail.index + 1} (attempt ${attempt}/${MAX_CHUNK_RETRIES}) in ${(retryDelay / 1e3).toFixed(0)}s...`);
679
+ await new Promise((r) => setTimeout(r, retryDelay));
680
+ if (isConnectionError(fail.error) && reconnect && reconnectionsUsed < MAX_RECONNECTIONS) {
681
+ try {
682
+ await doReconnect();
683
+ } catch (reconnectErr) {
684
+ console.log(` Reconnect failed: ${reconnectErr.message?.slice(0, 80)}`);
685
+ break;
686
+ }
687
+ }
688
+ try {
689
+ const currentNonce = await _fetchNonce(BULLETIN_ENDPOINTS, ss58);
690
+ const originalNonce = assignedNonces.get(fail.index);
691
+ if (originalNonce !== void 0 && originalNonce < currentNonce) {
692
+ console.log(` Chunk ${fail.index + 1}: nonce ${originalNonce} consumed (current=${currentNonce}), treating as included`);
693
+ stored[fail.index] = { cid: createCID(fail.chunkData, CID_CONFIG.codec, 18), len: fail.chunkData.length, viaFallback: true };
694
+ nonceAdvanceIndices.add(fail.index);
695
+ assignedNonces.delete(fail.index);
696
+ retried = true;
697
+ break;
698
+ }
699
+ const retryNonce = originalNonce ?? currentNonce;
700
+ const result2 = await storeChunk(unsafeApi, signer, fail.chunkData, retryNonce, ss58, { fetchNonce: fetchNonceOverride });
701
+ stored[fail.index] = result2;
702
+ assignedNonces.delete(fail.index);
703
+ retried = true;
704
+ break;
705
+ } catch (e) {
706
+ if (probeFailedCids && probeFailedCids.has(failCid.toString()) && e?.message?.includes("isValid:false")) {
707
+ console.log(` Chunk ${fail.index + 1}: retry isValid:false but CID was probe-failed \u2014 treating as already on chain`);
708
+ captureWarning("isValid:false retry treated as success (probe-failed backstop)", { chunkIndex: fail.index + 1, cid: failCid.toString(), attempt });
709
+ stored[fail.index] = { cid: failCid, len: fail.chunkData.length, viaFallback: true };
710
+ assignedNonces.delete(fail.index);
711
+ retried = true;
712
+ break;
713
+ }
714
+ captureWarning("Chunk retry failed", { chunkIndex: fail.index + 1, attempt, maxRetries: MAX_CHUNK_RETRIES, error: e.message?.slice(0, 200) });
715
+ console.log(` Retry ${attempt} failed: ${e.message?.slice(0, 80)}`);
716
+ if (isConnectionError(e) && reconnect && reconnectionsUsed < MAX_RECONNECTIONS) {
717
+ try {
718
+ await doReconnect();
719
+ } catch {
720
+ }
721
+ }
722
+ }
723
+ }
724
+ if (!retried) {
725
+ if (isConnectionError(fail.error) && reconnectionsUsed >= MAX_RECONNECTIONS) {
726
+ throw new Error(`Connection lost and max reconnections (${MAX_RECONNECTIONS}) exhausted`);
727
+ }
728
+ throw new Error(`Chunk ${fail.index + 1} failed after ${MAX_CHUNK_RETRIES} retries: ${fail.error?.message?.slice(0, 100)}`);
729
+ }
730
+ }
731
+ b += batchSize;
732
+ }
733
+ if (nonceAdvanceIndices.size > 0) {
734
+ const cidToIndex = new Map([...nonceAdvanceIndices].map((i) => [stored[i].cid.toString(), i]));
735
+ setDeployAttribute("deploy.pool.nonce_collision_count", nonceAdvanceIndices.size);
736
+ const probeResults = await probeChunks([...cidToIndex.keys()], { client });
737
+ const missingResults = probeResults.filter((r) => r.present === false);
738
+ setDeployAttribute("deploy.pool.nonce_collision_missing", missingResults.length);
739
+ if (missingResults.length > 0) {
740
+ captureWarning("nonce-advance collision: re-uploading missing chunks", {
741
+ collision_count: missingResults.length
742
+ });
743
+ }
744
+ let reuploadCount = 0;
745
+ for (const m of missingResults) {
746
+ const idx = cidToIndex.get(m.cid);
747
+ for (let attempt = 1; attempt <= MAX_REPROBE_RETRIES; attempt++) {
748
+ console.log(` Nonce-collision re-upload: chunk ${idx + 1} (attempt ${attempt}/${MAX_REPROBE_RETRIES})`);
749
+ try {
750
+ const freshNonce = await _fetchNonce(BULLETIN_ENDPOINTS, ss58);
751
+ const result2 = await storeChunk(unsafeApi, signer, chunks[idx], freshNonce, ss58, { fetchNonce: fetchNonceOverride });
752
+ stored[idx] = result2;
753
+ reuploadCount++;
754
+ break;
755
+ } catch (e) {
756
+ if (attempt === MAX_REPROBE_RETRIES) {
757
+ throw new Error(`Nonce-collision re-upload of chunk ${idx + 1} failed after ${MAX_REPROBE_RETRIES} attempts: ${e.message?.slice(0, 100)}`);
758
+ }
759
+ }
760
+ }
761
+ }
762
+ setDeployAttribute("deploy.pool.nonce_collision_reupload_count", reuploadCount);
763
+ }
764
+ setDeployAttribute("deploy.pool.account", truncateAddress(ss58));
765
+ const submittedCount = stored.filter((s) => !!s && !s.viaFallback).length;
766
+ console.log(submittedCount === 0 ? `
767
+ All ${chunks.length} chunks already on chain \u2014 nothing submitted` : `
768
+ All ${chunks.length} chunks included in block`);
769
+ console.log(` Verifying chunk integrity...`);
770
+ const missing = stored.map((c, i) => c === null ? i + 1 : null).filter(Boolean);
771
+ if (missing.length > 0) {
772
+ throw new Error(`Chunk verification failed: missing chunks at positions ${missing.join(", ")}`);
773
+ }
774
+ const verifiedStored = stored;
775
+ for (let i = 0; i < chunks.length; i++) {
776
+ const expectedCid = createCID(chunks[i], CID_CONFIG.codec, 18);
777
+ if (verifiedStored[i].cid.toString() !== expectedCid.toString()) {
778
+ throw new Error(`Chunk verification failed: chunk ${i + 1} CID mismatch (expected ${expectedCid}, got ${verifiedStored[i].cid})`);
779
+ }
780
+ }
781
+ console.log(` All ${chunks.length} chunks verified \u2713`);
782
+ console.log(` Building DAG-PB...`);
783
+ const fileData = new UnixFS({ type: "file", blockSizes: verifiedStored.map((c) => BigInt(c.len)) });
784
+ const dagNode = dagPB.prepare({ Data: fileData.marshal(), Links: verifiedStored.map((c) => ({ Name: "", Tsize: c.len, Hash: c.cid })) });
785
+ const dagBytes = dagPB.encode(dagNode);
786
+ const hashCode = 18;
787
+ const rootCid = createCID(dagBytes, 112, hashCode);
788
+ const rssBeforeRootMb = Math.round(process.memoryUsage().rss / 1024 / 1024);
789
+ if (rssBeforeRootMb > 2048) {
790
+ captureWarning("high RSS before root node store \u2014 OOM risk", {
791
+ rss_mb: rssBeforeRootMb,
792
+ chunks: chunks.length
793
+ });
794
+ }
795
+ let rootSkipped = false;
796
+ if (skipRootStore) {
797
+ rootSkipped = true;
798
+ } else {
799
+ const rootProbeResult = await probeChunks([rootCid.toString()], { client });
800
+ if (rootProbeResult[0]?.present === true) {
801
+ console.log(` Root node already on-chain (${rootCid.toString().slice(0, 20)}\u2026), skipping store.`);
802
+ rootSkipped = true;
803
+ }
804
+ }
805
+ let result;
806
+ let uploadReceipt;
807
+ if (rootSkipped) {
808
+ result = rootCid.toString();
809
+ uploadReceipt = stored.findLast((c) => c?.receipt)?.receipt;
810
+ } else {
811
+ const MAX_ROOT_RETRIES = 3;
812
+ for (let rootAttempt = 1; rootAttempt <= MAX_ROOT_RETRIES; rootAttempt++) {
813
+ const rootNonce = await _fetchNonce(BULLETIN_ENDPOINTS, ss58);
814
+ console.log(` Storing root node (nonce: ${rootNonce})...`);
815
+ const rootTx = unsafeApi.tx.TransactionStorage.store_with_cid_config({ cid: { codec: BigInt(112), hashing: toHashingEnum(hashCode) }, data: dagBytes });
816
+ const rootTxOpts = { mortality: { mortal: true, period: 256 }, nonce: rootNonce };
817
+ try {
818
+ const watchResult = await watchTransaction(rootTx, signer, rootTxOpts, () => {
819
+ console.log(` Root CID: ${rootCid.toString()}
820
+ `);
821
+ return rootCid.toString();
822
+ }, { label: "root-node", rpc: BULLETIN_ENDPOINTS, senderSS58: ss58, expectedNonce: rootNonce, timeoutMs: CHUNK_TIMEOUT_MS, fetchNonce: fetchNonceOverride });
823
+ result = watchResult.value;
824
+ uploadReceipt = watchResult.receipt;
825
+ break;
826
+ } catch (e) {
827
+ if (reconnect && reconnectionsUsed < MAX_RECONNECTIONS) {
828
+ await doReconnect();
829
+ continue;
830
+ }
831
+ if (rootAttempt < MAX_ROOT_RETRIES) {
832
+ console.log(` Root node attempt ${rootAttempt} failed: ${e.message?.slice(0, 80)}`);
833
+ await new Promise((r) => setTimeout(r, 6e3));
834
+ continue;
835
+ }
836
+ throw e;
837
+ }
838
+ }
839
+ }
840
+ if (uploadReceipt) {
841
+ setDeployAttribute("bulletin.upload.tx_hash", uploadReceipt.txHash);
842
+ setDeployAttribute("bulletin.upload.block_hash", uploadReceipt.blockHash);
843
+ setDeployAttribute("bulletin.upload.block_number", uploadReceipt.blockNumber);
844
+ console.log(` Storage upload finalised @ block ${uploadReceipt.blockNumber} (tx ${uploadReceipt.txHash})`);
845
+ }
846
+ if (wsHaltDetected && reconnect && reconnectionsUsed < MAX_RECONNECTIONS) {
847
+ wsHaltDetected = false;
848
+ await doReconnect();
849
+ ownsClient = false;
850
+ }
851
+ if (ownsClient) client.destroy();
852
+ return { storageCid: result, tier2Verified, tier2Inconclusive, tier2Fallback, liveProvider: { client, unsafeApi, signer, ss58 }, skipProbeResults, rootSkipped };
853
+ } catch (e) {
854
+ if (ownsClient) client.destroy();
855
+ throw e;
856
+ } finally {
857
+ setWsHaltCallback(null);
858
+ }
859
+ }
860
+ function chunk(data, size = CHUNK_SIZE) {
861
+ const chunks = [];
862
+ let offset = 0;
863
+ while (offset < data.length) {
864
+ const end = Math.min(offset + size, data.length);
865
+ chunks.push(data.subarray(offset, end));
866
+ offset = end;
867
+ }
868
+ return chunks;
869
+ }
870
+ var _hasIPFS;
871
+ function hasIPFS() {
872
+ if (_hasIPFS === void 0) {
873
+ try {
874
+ execSync("ipfs version", { stdio: "ignore" });
875
+ _hasIPFS = true;
876
+ } catch {
877
+ _hasIPFS = false;
878
+ }
879
+ }
880
+ return _hasIPFS;
881
+ }
882
+ async function merkleize(directoryPath, outputCarPath) {
883
+ if (!hasIPFS()) throw new Error("IPFS CLI not installed. Install from: https://docs.ipfs.tech/install/");
884
+ if (!fs.existsSync(directoryPath)) throw new Error(`Directory not found: ${directoryPath}`);
885
+ console.log(` Merkleizing: ${directoryPath}`);
886
+ const cid = execSync(`ipfs add -Q -r --cid-version=1 --raw-leaves --pin=false "${directoryPath}"`, { encoding: "utf-8" }).trim();
887
+ if (!cid) throw new Error("Failed to get CID from IPFS");
888
+ execSync(`ipfs dag export ${cid} > "${outputCarPath}"`);
889
+ if (!fs.existsSync(outputCarPath)) throw new Error("Failed to create CAR file");
890
+ const size = fs.statSync(outputCarPath).size;
891
+ console.log(` CAR: ${(size / 1024 / 1024).toFixed(2)} MB`);
892
+ return { carPath: outputCarPath, cid };
893
+ }
894
+ function computeStorageCid(chunks) {
895
+ const hashCode = 18;
896
+ const chunkInfo = chunks.map((c) => ({
897
+ cid: createCID(c, CID_CONFIG.codec, hashCode),
898
+ len: c.length
899
+ }));
900
+ const fileData = new UnixFS({ type: "file", blockSizes: chunkInfo.map((c) => BigInt(c.len)) });
901
+ const dagNode = dagPB.prepare({ Data: fileData.marshal(), Links: chunkInfo.map((c) => ({ Name: "", Tsize: c.len, Hash: c.cid })) });
902
+ const dagBytes = dagPB.encode(dagNode);
903
+ return createCID(dagBytes, 112, hashCode).toString();
904
+ }
905
+ async function storeDirectory(directoryPath, providerOrOptions = {}, password, jsMerkle) {
906
+ const opts = providerOrOptions && ("provider" in providerOrOptions || "onCarReady" in providerOrOptions || "password" in providerOrOptions || "jsMerkle" in providerOrOptions) ? providerOrOptions : { provider: providerOrOptions, password, jsMerkle };
907
+ const provider = opts.provider ?? {};
908
+ password = opts.password;
909
+ jsMerkle = opts.jsMerkle;
910
+ let carContent;
911
+ let ipfsCid;
912
+ const dirBasename = path.basename(directoryPath);
913
+ sampleMemory("storage_start");
914
+ if (jsMerkle) {
915
+ const result = await withSpan("deploy.merkleize", "1a. merkleize (js)", { "deploy.directory": dirBasename }, async () => {
916
+ const r = await merkleizeJS(directoryPath);
917
+ sampleMemory("merkleize_end");
918
+ return r;
919
+ });
920
+ carContent = result.carBytes;
921
+ ipfsCid = result.cid;
922
+ } else {
923
+ const carPath = path.join(path.dirname(directoryPath), `${path.basename(directoryPath)}.car`);
924
+ const { cid } = await withSpan("deploy.merkleize", "1a. merkleize", { "deploy.directory": dirBasename }, async () => {
925
+ const r = await merkleize(directoryPath, carPath);
926
+ sampleMemory("merkleize_end");
927
+ return r;
928
+ });
929
+ ipfsCid = cid;
930
+ carContent = fs.readFileSync(carPath);
931
+ }
932
+ if (password) {
933
+ console.log(` Encrypting CAR file...`);
934
+ carContent = await encryptContent(carContent, password);
935
+ console.log(` Encrypted: ${(carContent.length / 1024 / 1024).toFixed(2)} MB`);
936
+ }
937
+ const carDumpEnv = process.env.BULLETIN_DEPLOY_DUMP_CAR;
938
+ const carDumpOpt = opts.dumpCar;
939
+ if (carDumpEnv !== void 0 || carDumpOpt) {
940
+ const dumpPath = typeof carDumpEnv === "string" && carDumpEnv ? carDumpEnv : typeof carDumpOpt === "string" && carDumpOpt ? carDumpOpt : path.join(path.dirname(directoryPath), `${path.basename(directoryPath)}.bulletin.car`);
941
+ fs.writeFileSync(dumpPath, carContent);
942
+ console.log(` Pre-upload CAR saved to ${dumpPath} (${(carContent.length / 1024 / 1024).toFixed(2)} MB)`);
943
+ }
944
+ const carChunks = chunk(carContent, CHUNK_SIZE);
945
+ const predictedStorageCid = computeStorageCid(carChunks);
946
+ if (opts.onCarReady) await opts.onCarReady(carContent, predictedStorageCid);
947
+ setDeployReportContext({
948
+ jsMerkle: Boolean(jsMerkle),
949
+ chunkCount: carChunks.length,
950
+ carBytes: carContent.length,
951
+ outputDir: path.dirname(directoryPath)
952
+ });
953
+ setDeployContext({
954
+ chunkCount: carChunks.length,
955
+ totalSize: `${(carContent.length / 1024 / 1024).toFixed(2)} MB`
956
+ });
957
+ const carMb = String(Math.round(carContent.length / 1024 / 1024 * 100) / 100);
958
+ const storageCid = await withSpan("deploy.chunk-upload", "1b. chunk-upload", { "deploy.chunks.total": carChunks.length, "deploy.car.bytes": carContent.length, "deploy.car.mb": carMb }, async () => {
959
+ sampleMemory("chunk_upload_start");
960
+ const r = await storeChunkedContent(carChunks, provider);
961
+ sampleMemory("chunk_upload_end");
962
+ return r.storageCid;
963
+ });
964
+ if (storageCid !== predictedStorageCid) {
965
+ captureWarning("computeStorageCid drift vs storeChunkedContent", {
966
+ predicted: predictedStorageCid,
967
+ uploaded: storageCid
968
+ });
969
+ }
970
+ return { storageCid, ipfsCid, carBytes: carContent };
971
+ }
972
+ async function readPreviousContenthashSafe(dotns, domainName) {
973
+ try {
974
+ const hex = await dotns.getContenthash(domainName);
975
+ if (!hex || hex === "0x") return null;
976
+ const bytes = Buffer.from(hex.slice(2), "hex");
977
+ if (bytes[0] !== 227 || bytes.length < 4) return null;
978
+ return CID.decode(bytes.slice(2)).toString();
979
+ } catch {
980
+ return null;
981
+ }
982
+ }
983
+ function buildFilesMap(buildDir, fileCids = /* @__PURE__ */ new Map()) {
984
+ const map = {};
985
+ function walk(dir, prefix = "") {
986
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
987
+ entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
988
+ for (const entry of entries) {
989
+ const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
990
+ const abs = path.join(dir, entry.name);
991
+ if (entry.isDirectory()) walk(abs, rel);
992
+ else if (entry.isFile()) {
993
+ if (rel === MANIFEST_PATH) continue;
994
+ const fileCid = fileCids.get(rel) ?? "";
995
+ let size = 0;
996
+ try {
997
+ size = fs.statSync(abs).size;
998
+ } catch {
999
+ }
1000
+ const type = classifyFile(rel, { fileCid: fileCid || void 0 });
1001
+ map[rel] = { cid: fileCid, type, size };
1002
+ }
1003
+ }
1004
+ }
1005
+ walk(buildDir);
1006
+ return map;
1007
+ }
1008
+ async function readRetentionPeriodBlocks(unsafeApi) {
1009
+ try {
1010
+ const rp = await unsafeApi.query.TransactionStorage.RetentionPeriod.getValue();
1011
+ return Number(rp);
1012
+ } catch {
1013
+ return 0;
1014
+ }
1015
+ }
1016
+ function detectFramework(directoryPath) {
1017
+ if (fs.existsSync(path.join(directoryPath, "_next"))) return "next";
1018
+ if (fs.existsSync(path.join(directoryPath, "assets"))) return "vite";
1019
+ return null;
1020
+ }
1021
+ var SIZE_WARN_BYTES = 50 * 1024 * 1024;
1022
+ var SIZE_ABORT_BYTES = 500 * 1024 * 1024;
1023
+ function checkDeploySize(carBytes, opts) {
1024
+ if (carBytes >= SIZE_ABORT_BYTES && !opts.allowLargeDeploy) {
1025
+ return { kind: "abort", message: `deploy exceeds 500 MiB (${(carBytes / 1024 / 1024).toFixed(1)} MiB). Re-run with --allow-large-deploy if intentional.` };
1026
+ }
1027
+ if (carBytes >= SIZE_WARN_BYTES) {
1028
+ return { kind: "warn", message: `deploy exceeds 50 MiB (${(carBytes / 1024 / 1024).toFixed(1)} MiB). Continuing.` };
1029
+ }
1030
+ return { kind: "ok" };
1031
+ }
1032
+ function resolveReproducibleTimestamp(source) {
1033
+ if (source === "commit") {
1034
+ try {
1035
+ const out = execSync("git log -1 --format=%cI", { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
1036
+ const d2 = new Date(out);
1037
+ if (Number.isNaN(d2.getTime())) throw new Error("invalid git committer date");
1038
+ return d2.toISOString();
1039
+ } catch (e) {
1040
+ throw new Error(`--reproducible=commit failed: ${e?.message ?? e}. Provide --reproducible=<ISO8601> instead.`);
1041
+ }
1042
+ }
1043
+ if (source.startsWith("epoch:")) {
1044
+ const n = Number(source.slice("epoch:".length));
1045
+ if (!Number.isFinite(n)) throw new Error(`--reproducible=epoch:N requires a number; got ${source}`);
1046
+ return new Date(n * 1e3).toISOString();
1047
+ }
1048
+ const d = new Date(source);
1049
+ if (Number.isNaN(d.getTime())) throw new Error(`--reproducible=<source>: '${source}' is not a recognised timestamp`);
1050
+ return d.toISOString();
1051
+ }
1052
+ function preWarmGateway(chunkCids, gateways) {
1053
+ for (const cid of chunkCids) {
1054
+ for (const gw of gateways) {
1055
+ const url = `${gw.replace(/\/$/, "")}/ipfs/${cid}`;
1056
+ fetch(url, { method: "HEAD" }).catch(() => {
1057
+ });
1058
+ }
1059
+ }
1060
+ }
1061
+ function applyManifestFetchAttributes(fetched) {
1062
+ setDeployAttribute("deploy.manifest.fetch_source", fetched.source);
1063
+ setDeployAttribute("deploy.manifest.fetch_attempts", String(fetched.attempts ?? 0));
1064
+ setDeployAttribute("deploy.manifest.bytes_downloaded", String(fetched.bytesDownloaded ?? 0));
1065
+ }
1066
+ async function storeDirectoryV2(directoryPath, opts = {}) {
1067
+ if (opts.password) return storeDirectory(directoryPath, opts);
1068
+ const provider = opts.provider ?? {};
1069
+ const prevContenthash = opts.previousContenthash ?? null;
1070
+ const gateway = opts.gateway;
1071
+ const dirBasename = path.basename(directoryPath);
1072
+ sampleMemory("storage_start");
1073
+ const fetched = await fetchPreviousManifest(prevContenthash, {
1074
+ gateway,
1075
+ domain: opts.domain
1076
+ });
1077
+ const prevManifest = fetched.source === "embedded" ? fetched.manifest : null;
1078
+ console.log(` Manifest fetch: ${fetched.source}${fetched.source !== "none" ? ` (${fetched.attempts} attempt${fetched.attempts === 1 ? "" : "s"})` : ""}`);
1079
+ applyManifestFetchAttributes(fetched);
1080
+ const deployedAt = opts.reproducibleSource ? resolveReproducibleTimestamp(opts.reproducibleSource) : (/* @__PURE__ */ new Date()).toISOString();
1081
+ writeEmbeddedManifestPlaceholder(directoryPath, {
1082
+ version: MANIFEST_VERSION,
1083
+ previousContenthash: prevContenthash,
1084
+ deployedAt,
1085
+ framework: null
1086
+ });
1087
+ let useKubo;
1088
+ if (opts.jsMerkle === true) {
1089
+ useKubo = false;
1090
+ } else if (opts.jsMerkle === false) {
1091
+ if (!hasIPFS()) {
1092
+ throw new Error("jsMerkle:false requires the ipfs binary on PATH; install from https://docs.ipfs.tech/install/ or omit --js-merkle to fall back to JS.");
1093
+ }
1094
+ useKubo = true;
1095
+ } else {
1096
+ useKubo = hasIPFS();
1097
+ }
1098
+ const phaseA = await withSpan("deploy.merkleize", `1a. merkleize (${useKubo ? "kubo" : "js"}, stable)`, { "deploy.directory": dirBasename, "deploy.merkle": useKubo ? "kubo" : "js" }, async () => {
1099
+ const r = await merkleizeWithStableOrder(directoryPath, prevManifest?.stableBlockOrder, { useKubo });
1100
+ sampleMemory("merkleize_end");
1101
+ return r;
1102
+ });
1103
+ const carChunksA = phaseA.chunks;
1104
+ const carChunkCidsA = phaseA.chunkCids;
1105
+ const s1Start = phaseA.sectionChunkCounts.section0;
1106
+ const s1End = s1Start + phaseA.sectionChunkCounts.section1;
1107
+ const phaseAUploadChunks = carChunksA.slice(s1Start, s1End);
1108
+ const phaseAUploadCids = carChunkCidsA.slice(s1Start, s1End);
1109
+ const trustedCidsA = /* @__PURE__ */ new Set();
1110
+ if (prevManifest?.chunks) {
1111
+ for (const cid of Object.keys(prevManifest.chunks)) {
1112
+ trustedCidsA.add(cid);
1113
+ }
1114
+ }
1115
+ const skipCidsA = new Set(phaseAUploadCids);
1116
+ const probeFailedCidsA = /* @__PURE__ */ new Set();
1117
+ const hasNewChunks = phaseAUploadCids.some((c) => !trustedCidsA.has(c));
1118
+ setDeployAttribute("deploy.phase_a.chunks_trusted", trustedCidsA.size);
1119
+ setDeployReportContext({
1120
+ jsMerkle: true,
1121
+ chunkCount: carChunksA.length,
1122
+ carBytes: phaseA.carBytes.length,
1123
+ outputDir: path.dirname(directoryPath)
1124
+ });
1125
+ setDeployContext({
1126
+ chunkCount: carChunksA.length,
1127
+ totalSize: `${(phaseA.carBytes.length / 1024 / 1024).toFixed(2)} MB`
1128
+ });
1129
+ const carMbA = String(Math.round(phaseA.carBytes.length / 1024 / 1024 * 100) / 100);
1130
+ let phaseALiveProvider = provider;
1131
+ let phaseASkipProbeResults = /* @__PURE__ */ new Map();
1132
+ if (!hasNewChunks) {
1133
+ console.log(` Phase A: nothing to upload (all ${phaseAUploadCids.length} section-1 chunks trusted from prev manifest)`);
1134
+ phaseASkipProbeResults = new Map(phaseAUploadCids.map((c) => [c, true]));
1135
+ } else {
1136
+ const trustedCount = phaseAUploadCids.length - skipCidsA.size;
1137
+ if (trustedCount > 0) {
1138
+ console.log(` Phase A: ${skipCidsA.size} new chunks to upload, ${trustedCount} trusted from prev manifest`);
1139
+ }
1140
+ await withSpan("deploy.chunk-upload", "1b. chunk-upload (phase A)", {
1141
+ "deploy.chunks.total": phaseAUploadChunks.length,
1142
+ "deploy.car.bytes": phaseA.carBytes.length,
1143
+ "deploy.car.mb": carMbA
1144
+ }, async () => {
1145
+ sampleMemory("chunk_upload_start");
1146
+ const phaseAUpload = await storeChunkedContent(phaseAUploadChunks, { ...provider, gateway, skipCids: skipCidsA, trustedCids: trustedCidsA, skipRootStore: true });
1147
+ phaseALiveProvider = { ...provider, ...phaseAUpload.liveProvider };
1148
+ phaseASkipProbeResults = phaseAUpload.skipProbeResults;
1149
+ setDeployAttribute("deploy.storage.phase_a.root_already_onchain", String(phaseAUpload.rootSkipped));
1150
+ if (phaseAUpload.tier2Inconclusive > 0) {
1151
+ captureWarning("Phase A chunk probe inconclusive \u2014 chain RPC returned null for some CIDs", {
1152
+ tier2Inconclusive: phaseAUpload.tier2Inconclusive,
1153
+ total: phaseAUploadChunks.length
1154
+ });
1155
+ }
1156
+ sampleMemory("chunk_upload_end");
1157
+ });
1158
+ }
1159
+ let probePresent = 0;
1160
+ let probeAbsent = 0;
1161
+ let bytesProbePresent = 0;
1162
+ let bytesProbeAbsent = 0;
1163
+ for (let i = 0; i < phaseAUploadCids.length; i++) {
1164
+ const cid = phaseAUploadCids[i];
1165
+ if (trustedCidsA.has(cid)) {
1166
+ probePresent++;
1167
+ bytesProbePresent += phaseAUploadChunks[i].length;
1168
+ continue;
1169
+ }
1170
+ const present = phaseASkipProbeResults.get(cid);
1171
+ if (present === true) {
1172
+ probePresent++;
1173
+ bytesProbePresent += phaseAUploadChunks[i].length;
1174
+ } else if (present === false) {
1175
+ probeAbsent++;
1176
+ bytesProbeAbsent += phaseAUploadChunks[i].length;
1177
+ } else if (present === null) {
1178
+ probeFailedCidsA.add(cid);
1179
+ }
1180
+ }
1181
+ const probeFailedCount = probeFailedCidsA.size;
1182
+ setDeployAttribute("deploy.probe.present", probePresent);
1183
+ setDeployAttribute("deploy.probe.absent", probeAbsent);
1184
+ setDeployAttribute("deploy.probe.failed", probeFailedCount);
1185
+ setDeployAttribute("deploy.phase_a.chunks_uploaded", probeAbsent);
1186
+ const phaseAKnownPresent = new Set(phaseAUploadCids);
1187
+ const filesMap = buildFilesMap(directoryPath, phaseA.fileCids);
1188
+ const blocksList = [...phaseA.blocks.keys()];
1189
+ const chunksMap = {};
1190
+ for (let i = 0; i < phaseA.section1ChunkCids.length; i++) {
1191
+ const cid = phaseA.section1ChunkCids[i];
1192
+ const probePresence = phaseASkipProbeResults.get(cid);
1193
+ const inheritFrom = prevManifest?.chunks?.[cid];
1194
+ let deployedAtForChunk;
1195
+ if ((probePresence === true || probePresence === null) && inheritFrom) {
1196
+ deployedAtForChunk = inheritFrom.deployed_at;
1197
+ } else {
1198
+ deployedAtForChunk = deployedAt;
1199
+ }
1200
+ const probedIdx = phaseA.chunkCids.indexOf(cid);
1201
+ const sizeBytes = phaseA.chunks[probedIdx]?.length ?? 0;
1202
+ chunksMap[cid] = { size: sizeBytes, deployed_at: deployedAtForChunk };
1203
+ }
1204
+ finaliseEmbeddedManifest(directoryPath, {
1205
+ version: MANIFEST_VERSION,
1206
+ previousContenthash: prevContenthash,
1207
+ deployedAt,
1208
+ framework: detectFramework(directoryPath),
1209
+ files: filesMap,
1210
+ stableBlockOrder: phaseA.stableOrder,
1211
+ blocks: blocksList,
1212
+ chunks: chunksMap
1213
+ });
1214
+ phaseA.blocks.clear();
1215
+ carChunksA.length = 0;
1216
+ phaseA.carBytes = new Uint8Array(0);
1217
+ const phaseB = await withSpan("deploy.merkleize", "1c. merkleize (js, finalise)", { "deploy.directory": dirBasename }, async () => {
1218
+ const r = await merkleizeWithStableOrder(directoryPath, phaseA.stableOrder, { useKubo });
1219
+ sampleMemory("merkleize_finalise_end");
1220
+ return r;
1221
+ });
1222
+ const sizeDecision = checkDeploySize(phaseB.carBytes.length, { allowLargeDeploy: opts.allowLargeDeploy });
1223
+ if (sizeDecision.kind === "abort") throw new Error(sizeDecision.message);
1224
+ if (sizeDecision.kind === "warn") console.warn(` \u26A0 ${sizeDecision.message}`);
1225
+ const carDumpEnv = process.env.BULLETIN_DEPLOY_DUMP_CAR;
1226
+ const carDumpOpt = opts.dumpCar;
1227
+ if (carDumpEnv !== void 0 || carDumpOpt) {
1228
+ const dumpPath = typeof carDumpEnv === "string" && carDumpEnv ? carDumpEnv : typeof carDumpOpt === "string" && carDumpOpt ? carDumpOpt : path.join(path.dirname(directoryPath), `${path.basename(directoryPath)}.bulletin.car`);
1229
+ fs.writeFileSync(dumpPath, phaseB.carBytes);
1230
+ console.log(` Pre-upload CAR saved to ${dumpPath} (${(phaseB.carBytes.length / 1024 / 1024).toFixed(2)} MB)`);
1231
+ }
1232
+ const carChunksB = phaseB.chunks;
1233
+ const carChunkCidsB = phaseB.chunkCids;
1234
+ const trustedCidsB = new Set(phaseAKnownPresent);
1235
+ let phaseBProbeHits = 0;
1236
+ {
1237
+ const phaseBUnknown = carChunkCidsB.filter((c) => !trustedCidsB.has(c));
1238
+ if (phaseBUnknown.length > 0) {
1239
+ const probeResults = await probeChunks(phaseBUnknown, { client: phaseALiveProvider.client });
1240
+ for (const r of probeResults) {
1241
+ if (r.present === true) {
1242
+ trustedCidsB.add(r.cid);
1243
+ phaseBProbeHits++;
1244
+ }
1245
+ }
1246
+ }
1247
+ }
1248
+ const predictedStorageCid = computeStorageCid(carChunksB);
1249
+ if (opts.onCarReady) await opts.onCarReady(phaseB.carBytes, predictedStorageCid);
1250
+ const carMbB = String(Math.round(phaseB.carBytes.length / 1024 / 1024 * 100) / 100);
1251
+ const newPhaseBChunks = carChunkCidsB.filter((c) => !trustedCidsB.has(c)).length;
1252
+ const phaseBResult = await withSpan("deploy.chunk-upload", "1d. chunk-upload (phase B)", {
1253
+ "deploy.chunks.total": carChunksB.length,
1254
+ "deploy.chunks.phase_b_new": newPhaseBChunks,
1255
+ "deploy.car.bytes": phaseB.carBytes.length,
1256
+ "deploy.car.mb": carMbB
1257
+ }, async () => {
1258
+ sampleMemory("chunk_upload_b_start");
1259
+ const r = await storeChunkedContent(carChunksB, { ...phaseALiveProvider, gateway, trustedCids: trustedCidsB, probeFailedCids: probeFailedCidsA });
1260
+ sampleMemory("chunk_upload_b_end");
1261
+ return r;
1262
+ });
1263
+ const storageCid = phaseBResult.storageCid;
1264
+ setDeployAttribute("deploy.storage.phase_b.probe_hit_count", phaseBProbeHits);
1265
+ {
1266
+ const grandpaCids = [...phaseB.chunkCids, storageCid];
1267
+ console.log(` Finality check: probing ${grandpaCids.length} chunks at chain-finalised state (aka GRANDPA)...`);
1268
+ const finalityResults = await probeChunks(grandpaCids, { client: phaseALiveProvider.client, atFinalized: true });
1269
+ let missingCids = new Set(finalityResults.filter((r) => r.present === false).map((r) => r.cid));
1270
+ setDeployAttribute("deploy.probe.finality_miss_count", missingCids.size);
1271
+ let reuploadCount = 0;
1272
+ if (missingCids.size === 0) {
1273
+ console.log(` \u2713 All ${grandpaCids.length} chunks finalised`);
1274
+ } else {
1275
+ console.log(` ${missingCids.size} chunks not yet finalised \u2014 waiting up to ${GRANDPA_NATURAL_WAIT_MS / 1e3}s for natural finalisation`);
1276
+ for (const cid of missingCids) console.log(` ${cid.slice(0, 20)}\u2026`);
1277
+ const waitStart = Date.now();
1278
+ while (Date.now() - waitStart < GRANDPA_NATURAL_WAIT_MS && missingCids.size > 0) {
1279
+ await new Promise((r) => setTimeout(r, GRANDPA_REUPLOAD_POLL_MS));
1280
+ const poll = await probeChunks([...missingCids], { client: phaseALiveProvider.client, atFinalized: true });
1281
+ for (const r of poll) {
1282
+ if (r.present === true) missingCids.delete(r.cid);
1283
+ }
1284
+ }
1285
+ if (missingCids.size === 0) {
1286
+ const elapsed = Math.round((Date.now() - waitStart) / 1e3);
1287
+ console.log(` \u2713 All ${grandpaCids.length} chunks finalised (waited ${elapsed}s)`);
1288
+ } else {
1289
+ const rootHashCode = 18;
1290
+ const rootChunkLinks = phaseB.chunks.map((c) => ({
1291
+ cid: createCID(c, CID_CONFIG.codec, rootHashCode),
1292
+ len: c.length
1293
+ }));
1294
+ const rootFileData = new UnixFS({ type: "file", blockSizes: rootChunkLinks.map((c) => BigInt(c.len)) });
1295
+ const rootDagNode = dagPB.prepare({ Data: rootFileData.marshal(), Links: rootChunkLinks.map((c) => ({ Name: "", Tsize: c.len, Hash: c.cid })) });
1296
+ const rootDagBytes = dagPB.encode(rootDagNode);
1297
+ const phaseBChunkByCid = /* @__PURE__ */ new Map();
1298
+ for (let i = 0; i < phaseB.chunkCids.length; i++) {
1299
+ phaseBChunkByCid.set(phaseB.chunkCids[i], phaseB.chunks[i]);
1300
+ }
1301
+ const fetchNonceFn = phaseALiveProvider.fetchNonce ?? fetchNonce;
1302
+ for (let round = 1; round <= GRANDPA_REUPLOAD_MAX_ROUNDS && missingCids.size > 0; round++) {
1303
+ const roundSuffix = round > 1 ? ` (round ${round}/${GRANDPA_REUPLOAD_MAX_ROUNDS}, retry after fork)` : "";
1304
+ console.log(` ${missingCids.size} chunks still missing after wait \u2014 re-uploading${roundSuffix}`);
1305
+ const reuploadList = [...missingCids];
1306
+ for (let i = 0; i < reuploadList.length; i++) {
1307
+ const cid = reuploadList[i];
1308
+ const freshNonce = await fetchNonceFn(BULLETIN_ENDPOINTS, phaseALiveProvider.ss58);
1309
+ if (cid === storageCid) {
1310
+ const rootTx = phaseALiveProvider.unsafeApi.tx.TransactionStorage.store_with_cid_config({
1311
+ cid: { codec: BigInt(112), hashing: toHashingEnum(rootHashCode) },
1312
+ data: rootDagBytes
1313
+ });
1314
+ await watchTransaction(rootTx, phaseALiveProvider.signer, { mortality: { mortal: true, period: 256 }, nonce: freshNonce }, () => storageCid, {
1315
+ label: "root-reupload",
1316
+ rpc: BULLETIN_ENDPOINTS,
1317
+ senderSS58: phaseALiveProvider.ss58,
1318
+ expectedNonce: freshNonce,
1319
+ timeoutMs: CHUNK_TIMEOUT_MS,
1320
+ fetchNonce: phaseALiveProvider.fetchNonce
1321
+ });
1322
+ } else {
1323
+ const chunkBytes = phaseBChunkByCid.get(cid);
1324
+ if (!chunkBytes) {
1325
+ throw new Error(
1326
+ `Deploy verification failed: chunk ${cid.slice(0, 20)}\u2026 missing at finalised head and its bytes are not in phaseB.chunks (cannot re-upload). This indicates an internal state issue.`
1327
+ );
1328
+ }
1329
+ await storeChunk(phaseALiveProvider.unsafeApi, phaseALiveProvider.signer, chunkBytes, freshNonce, phaseALiveProvider.ss58, { fetchNonce: phaseALiveProvider.fetchNonce });
1330
+ }
1331
+ reuploadCount++;
1332
+ console.log(` [${i + 1}/${reuploadList.length}] re-uploaded ${cid.slice(0, 20)}\u2026 (nonce ${freshNonce})`);
1333
+ }
1334
+ const reuploadStart = Date.now();
1335
+ while (Date.now() - reuploadStart < GRANDPA_REUPLOAD_TIMEOUT_MS && missingCids.size > 0) {
1336
+ await new Promise((r) => setTimeout(r, GRANDPA_REUPLOAD_POLL_MS));
1337
+ const poll = await probeChunks([...missingCids], { client: phaseALiveProvider.client, atFinalized: true });
1338
+ for (const r of poll) {
1339
+ if (r.present === true) missingCids.delete(r.cid);
1340
+ }
1341
+ }
1342
+ }
1343
+ if (missingCids.size > 0) {
1344
+ const stuck = [...missingCids][0];
1345
+ throw new Error(
1346
+ `Deploy verification failed: ${missingCids.size} chunk(s) not finalised after ${GRANDPA_REUPLOAD_MAX_ROUNDS} re-upload round(s) (first: ${stuck.slice(0, 20)}\u2026). The chain may have dropped chunks due to a persistent fork. Re-run deploy.`
1347
+ );
1348
+ }
1349
+ console.log(` \u2713 All ${grandpaCids.length} chunks finalised after re-upload`);
1350
+ }
1351
+ }
1352
+ setDeployAttribute("deploy.probe.finality_miss_reupload_count", reuploadCount);
1353
+ }
1354
+ if (opts.domain) {
1355
+ try {
1356
+ const manifestText = fs.readFileSync(path.join(directoryPath, MANIFEST_PATH), "utf8");
1357
+ writePersistentLocalManifest(opts.domain, storageCid, manifestText);
1358
+ } catch {
1359
+ }
1360
+ }
1361
+ if (storageCid !== predictedStorageCid) {
1362
+ captureWarning("computeStorageCid drift vs storeChunkedContent (v2)", {
1363
+ predicted: predictedStorageCid,
1364
+ uploaded: storageCid
1365
+ });
1366
+ }
1367
+ const newlyUploadedCids = carChunkCidsB.filter((c) => !trustedCidsB.has(c));
1368
+ if (newlyUploadedCids.length > 0 && gateway) {
1369
+ preWarmGateway(newlyUploadedCids, [gateway]);
1370
+ }
1371
+ const retentionPeriodBlocks = await readRetentionPeriodBlocks(provider.unsafeApi);
1372
+ const framework = detectFramework(directoryPath);
1373
+ const filesStableCount = [...phaseA.fileCids.entries()].filter(([p, cid]) => {
1374
+ if (p === MANIFEST_PATH) return false;
1375
+ return classifyFile(p, { prevManifest, fileCid: cid, framework }) === "stable";
1376
+ }).length;
1377
+ const filesTotalCount = phaseA.fileCids.size - (phaseA.fileCids.has(MANIFEST_PATH) ? 1 : 0);
1378
+ const probeResultsForStats = carChunkCidsA.flatMap((cid) => {
1379
+ if (trustedCidsA.has(cid)) return [{ cid, present: true, block: 0, index: 0 }];
1380
+ if (!phaseASkipProbeResults.has(cid)) return [];
1381
+ const present = phaseASkipProbeResults.get(cid);
1382
+ if (present === true) return [{ cid, present: true, block: 0, index: 0 }];
1383
+ if (present === false) return [{ cid, present: false }];
1384
+ return [{ cid, present: null, failureReason: "rpc_error" }];
1385
+ });
1386
+ let phaseBChunksUploaded = 0;
1387
+ let phaseBBytesUploaded = 0;
1388
+ for (let i = 0; i < phaseB.chunks.length; i++) {
1389
+ if (!trustedCidsB.has(phaseB.chunkCids[i])) {
1390
+ phaseBChunksUploaded++;
1391
+ phaseBBytesUploaded += phaseB.chunks[i].length;
1392
+ }
1393
+ }
1394
+ const chunksUploadedTotal = probeAbsent + phaseBChunksUploaded;
1395
+ const bytesUploadedTotal = bytesProbeAbsent + phaseBBytesUploaded;
1396
+ const chunksSkippedTotal = phaseB.chunks.length - chunksUploadedTotal;
1397
+ const bytesSkippedTotal = phaseB.carBytes.length - bytesUploadedTotal;
1398
+ const stats = computeStats({
1399
+ manifestSource: fetched.source,
1400
+ manifestFetchAttempts: fetched.source === "none" ? 0 : fetched.attempts ?? 0,
1401
+ manifestBytes: fetched.source === "embedded" ? fetched.bytesDownloaded ?? 0 : 0,
1402
+ framework,
1403
+ filesTotal: filesTotalCount,
1404
+ filesStable: filesStableCount,
1405
+ filesVolatile: filesTotalCount - filesStableCount,
1406
+ probeResults: probeResultsForStats,
1407
+ prevChunks: prevManifest?.chunks ?? {},
1408
+ retentionPeriodBlocks,
1409
+ bytesProbePresent,
1410
+ bytesProbeAbsent,
1411
+ bytesSkipped: bytesSkippedTotal,
1412
+ bytesUploaded: bytesUploadedTotal,
1413
+ chunksTotal: phaseB.chunks.length,
1414
+ chunksUploaded: chunksUploadedTotal,
1415
+ chunksSkipped: chunksSkippedTotal,
1416
+ carBytes: phaseB.carBytes.length,
1417
+ sectionSizes: phaseB.sectionSizes,
1418
+ tier2VerifiedCount: phaseBResult.tier2Verified,
1419
+ tier2InconclusiveCount: phaseBResult.tier2Inconclusive,
1420
+ tier2FallbackCount: phaseBResult.tier2Fallback
1421
+ });
1422
+ for (const [k, v] of Object.entries(telemetryAttributes(stats))) {
1423
+ setDeployAttribute(k, v);
1424
+ }
1425
+ console.log("\n" + renderSummary(stats));
1426
+ console.log(` Final root check: ${storageCid}`);
1427
+ const rootProbe = await probeChunks([storageCid], { client: phaseALiveProvider.client, atFinalized: true });
1428
+ if (rootProbe[0]?.present === false) {
1429
+ throw new Error(
1430
+ `Deploy verification failed: DAG-PB root ${storageCid.slice(0, 20)}\u2026 not finalised. The chain may have evicted the root extrinsic. Re-run deploy.`
1431
+ );
1432
+ }
1433
+ if (rootProbe[0]?.present === true) {
1434
+ console.log(` \u2713 Root finalised on chain`);
1435
+ } else {
1436
+ console.log(` Root re-check inconclusive (RPC error) \u2014 GRANDPA probe above already verified; continuing.`);
1437
+ }
1438
+ return { storageCid, ipfsCid: phaseB.cid, carBytes: phaseB.carBytes };
1439
+ }
1440
+ function resolveDotnsConnectOptions(options, assetHubEndpoints, autoAccountMapping, contracts, nativeToEthRatio, environmentId, popSelfServe, registerStorageDeposit) {
1441
+ const tail = assetHubEndpoints && assetHubEndpoints.length > 0 ? { assetHubEndpoints } : {};
1442
+ const mappingTail = autoAccountMapping ? { autoAccountMapping } : {};
1443
+ const contractsTail = contracts && Object.keys(contracts).length > 0 ? { contracts } : {};
1444
+ const ratioTail = nativeToEthRatio ? { nativeToEthRatio } : {};
1445
+ const envTail = environmentId ? { environmentId } : {};
1446
+ const popTail = popSelfServe !== void 0 ? { popSelfServe } : {};
1447
+ const storageTail = registerStorageDeposit !== void 0 ? { registerStorageDeposit } : {};
1448
+ if (options.signer && options.signerAddress) {
1449
+ return { signer: options.signer, signerAddress: options.signerAddress, ...tail, ...mappingTail, ...contractsTail, ...ratioTail, ...envTail, ...popTail, ...storageTail };
1450
+ }
1451
+ return { mnemonic: options.mnemonic, derivationPath: options.derivationPath, ...tail, ...mappingTail, ...contractsTail, ...ratioTail, ...envTail, ...popTail, ...storageTail };
1452
+ }
1453
+ async function estimateUploadBytes(content) {
1454
+ try {
1455
+ if (Array.isArray(content)) {
1456
+ return content.reduce((s, c) => s + c.length, 0);
1457
+ }
1458
+ if (content instanceof Uint8Array) {
1459
+ return content.length;
1460
+ }
1461
+ const resolved = path.resolve(content);
1462
+ if (!fs.existsSync(resolved)) return null;
1463
+ const st = fs.statSync(resolved);
1464
+ if (st.isFile()) return st.size;
1465
+ let total = 0;
1466
+ const walk = (dir) => {
1467
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
1468
+ const child = path.join(dir, entry.name);
1469
+ if (entry.isDirectory()) walk(child);
1470
+ else if (entry.isFile()) total += fs.statSync(child).size;
1471
+ }
1472
+ };
1473
+ walk(resolved);
1474
+ return total;
1475
+ } catch {
1476
+ return null;
1477
+ }
1478
+ }
1479
+ function assertSubdomainOwnerMatchesSigner(result, signerEvmAddress, sublabel, parentLabel) {
1480
+ if (result.owned && result.owner?.toLowerCase() !== signerEvmAddress?.toLowerCase()) {
1481
+ throw new NonRetryableError(
1482
+ `Subdomain ${sublabel}.${parentLabel}.dot is already owned by ${result.owner} (signer is ${signerEvmAddress}). Use a fresh subdomain label, or release the existing registration.`
1483
+ );
1484
+ }
1485
+ }
1486
+ async function publish(dotns, parsed, failOnError) {
1487
+ console.log("\n" + "=".repeat(60));
1488
+ console.log("Publish");
1489
+ console.log("=".repeat(60));
1490
+ if (parsed.isSubdomain) {
1491
+ console.log(` Subdomains are not supported by the Publisher registry \u2014 skipping.`);
1492
+ return;
1493
+ }
1494
+ try {
1495
+ const result = await dotns.publishLabel(parsed.label);
1496
+ setDeployAttribute("deploy.publish.status", result.status);
1497
+ if (result.txHash) setDeployAttribute("deploy.publish.tx", result.txHash);
1498
+ console.log(` Status: ${result.status}`);
1499
+ } catch (e) {
1500
+ if (e instanceof PublisherNotSupportedError) {
1501
+ console.log(` Skipped: ${e.message}`);
1502
+ return;
1503
+ }
1504
+ setDeployAttribute("deploy.publish.status", "failed");
1505
+ if (failOnError) throw e;
1506
+ const msg = e?.message ?? String(e);
1507
+ console.error(` Warning: publish failed (non-fatal): ${msg}`);
1508
+ captureWarning("publish failed", { error: msg.slice(0, 200) });
1509
+ }
1510
+ }
1511
+ async function unpublish(domainName, options = {}) {
1512
+ const envId = options.env ?? DEFAULT_ENV_ID;
1513
+ const { doc } = await loadEnvironments();
1514
+ const resolved = resolveEndpoints(doc, envId);
1515
+ const popSelfServe = getPopSelfServeConfig(doc, envId);
1516
+ const parsed = parseDomainName(domainName);
1517
+ if (parsed.isSubdomain) {
1518
+ throw new Error(`Subdomains are not supported by the Publisher registry. To unpublish ${parsed.parentLabel}.dot (which controls ${domainName}), pass that label directly.`);
1519
+ }
1520
+ const label = parsed.label;
1521
+ const dotns = new DotNS();
1522
+ try {
1523
+ await dotns.connect(resolveDotnsConnectOptions(
1524
+ { mnemonic: options.mnemonic, derivationPath: options.derivationPath },
1525
+ resolved.assetHub,
1526
+ resolved.autoAccountMapping,
1527
+ resolved.contracts,
1528
+ resolved.nativeToEthRatio,
1529
+ envId,
1530
+ popSelfServe,
1531
+ resolved.registerStorageDeposit
1532
+ ));
1533
+ const result = await dotns.unpublishLabel(label);
1534
+ return { domainName: `${label}.dot`, status: result.status, txHash: result.txHash };
1535
+ } finally {
1536
+ try {
1537
+ dotns.disconnect();
1538
+ } catch {
1539
+ }
1540
+ }
1541
+ }
1542
+ async function deploy(content, domainName = null, options = {}) {
1543
+ const envId = options.env ?? DEFAULT_ENV_ID;
1544
+ let envBulletin = [DEFAULT_BULLETIN_RPC];
1545
+ let envAssetHub;
1546
+ let envSource;
1547
+ let envNetwork;
1548
+ let envName;
1549
+ let envIpfs;
1550
+ let envAutoAccountMapping = false;
1551
+ let envContracts = {};
1552
+ let envBulletinAuthorizeV2 = false;
1553
+ let envNativeToEthRatio;
1554
+ let envRegisterStorageDeposit;
1555
+ let envPopSelfServe = null;
1556
+ if (options.bulletinEndpoints && options.bulletinEndpoints.length > 0) {
1557
+ envBulletin = options.bulletinEndpoints;
1558
+ envAssetHub = options.assetHubEndpoints;
1559
+ } else {
1560
+ try {
1561
+ const { doc, source } = await loadEnvironments();
1562
+ const resolved = resolveEndpoints(doc, envId);
1563
+ envBulletin = resolved.bulletin;
1564
+ envAssetHub = options.assetHubEndpoints ?? resolved.assetHub;
1565
+ envSource = source;
1566
+ envNetwork = resolved.network;
1567
+ envName = resolved.envName;
1568
+ envIpfs = resolved.ipfs;
1569
+ envAutoAccountMapping = resolved.autoAccountMapping;
1570
+ envContracts = resolved.contracts;
1571
+ envBulletinAuthorizeV2 = resolved.bulletinAuthorizeV2;
1572
+ envNativeToEthRatio = resolved.nativeToEthRatio;
1573
+ envRegisterStorageDeposit = resolved.registerStorageDeposit;
1574
+ envPopSelfServe = getPopSelfServeConfig(doc, envId);
1575
+ } catch (e) {
1576
+ if (e instanceof NonRetryableError) throw e;
1577
+ if (options.env !== void 0) throw e;
1578
+ captureWarning(`environments load failed: ${e?.message ?? e}`);
1579
+ }
1580
+ }
1581
+ const userRpc = options.rpc ?? process.env.BULLETIN_RPC;
1582
+ BULLETIN_ENDPOINTS = userRpc ? [userRpc, ...envBulletin.filter((e) => e !== userRpc)] : envBulletin;
1583
+ _deployRpcFailedOver = false;
1584
+ POOL_SIZE = options.poolSize ?? parseInt(process.env.BULLETIN_POOL_SIZE ?? String(DEFAULT_POOL_SIZE), 10);
1585
+ initTelemetry();
1586
+ const randomSuffix = Math.floor(Math.random() * 100).toString().padStart(2, "0");
1587
+ const parsed = domainName ? parseDomainName(domainName) : null;
1588
+ const name = parsed ? parsed.label : `test-domain-${Date.now().toString(36)}${randomSuffix}`;
1589
+ return withDeploySpan(name, async () => {
1590
+ const deployTag = options.tag ?? process.env.DEPLOY_TAG;
1591
+ if (deployTag) {
1592
+ setDeployAttribute("deploy.tag", deployTag);
1593
+ setDeploySentryTag("deploy.tag", deployTag);
1594
+ }
1595
+ setDeployAttribute("deploy.env", envId);
1596
+ setDeployAttribute("deploy.label", parsed?.label ?? name);
1597
+ setDeployAttribute("deploy.subdomain", String(parsed?.isSubdomain ?? false));
1598
+ if (envNetwork) setDeployAttribute("deploy.network", envNetwork);
1599
+ if (envSource) setDeployAttribute("deploy.environments_source", envSource);
1600
+ let cid;
1601
+ let ipfsCid;
1602
+ let mirrorPromise = Promise.resolve(null);
1603
+ console.log("\n" + "=".repeat(60));
1604
+ console.log(`DEPLOYING TO TESTNET v${VERSION}`);
1605
+ console.log("=".repeat(60));
1606
+ if (envName) console.log(` Environment: ${envName}`);
1607
+ console.log(` Domain: ${name}.dot`);
1608
+ if (deployTag) console.log(` Tag: ${deployTag}`);
1609
+ if (options.inputCar) console.log(` Input CAR: ${path.resolve(options.inputCar)}`);
1610
+ else if (typeof content === "string") console.log(` Build dir: ${path.resolve(content)}`);
1611
+ if (process.env.CI) console.log(` Runner: ${resolveRunner()} (${resolveRunnerType()})`);
1612
+ if (options.password) console.log(` Encrypted: yes`);
1613
+ let provider;
1614
+ const reconnect = options.mnemonic ? () => getDirectProvider(options.mnemonic, options.derivationPath, envBulletinAuthorizeV2) : () => getProvider();
1615
+ let dotnsPreflight = null;
1616
+ let previousContenthashCid = null;
1617
+ try {
1618
+ console.log("\n" + "=".repeat(60));
1619
+ console.log("Preflight");
1620
+ console.log("=".repeat(60));
1621
+ const preflight = new DotNS();
1622
+ await preflight.connect(resolveDotnsConnectOptions(options, envAssetHub, envAutoAccountMapping, envContracts, envNativeToEthRatio, envId, envPopSelfServe, envRegisterStorageDeposit));
1623
+ if (parsed?.isSubdomain) {
1624
+ try {
1625
+ const subResult = await preflight.checkSubdomainOwnership(parsed.sublabel, parsed.parentLabel);
1626
+ assertSubdomainOwnerMatchesSigner(subResult, preflight.evmAddress, parsed.sublabel, parsed.parentLabel);
1627
+ if (!subResult.owned) {
1628
+ const { owned: parentOwned, owner: parentOwner } = await preflight.checkOwnership(parsed.parentLabel);
1629
+ if (!parentOwned) {
1630
+ throw new NonRetryableError(
1631
+ `Cannot deploy ${parsed.fullName}: parent ${parsed.parentLabel}.dot is owned by ${parentOwner ?? "no one"}, not by this signer.`
1632
+ );
1633
+ }
1634
+ }
1635
+ previousContenthashCid = await readPreviousContenthashSafe(preflight, parsed.fullName);
1636
+ setDeployAttribute("deploy.incremental", previousContenthashCid ? "true" : "false");
1637
+ } finally {
1638
+ preflight.disconnect();
1639
+ }
1640
+ console.log(` Mode: subdomain (parent ${parsed.parentLabel}.dot owned by signer)`);
1641
+ } else {
1642
+ try {
1643
+ dotnsPreflight = await preflight.preflight(name);
1644
+ previousContenthashCid = await readPreviousContenthashSafe(preflight, name);
1645
+ setDeployAttribute("deploy.incremental", previousContenthashCid ? "true" : "false");
1646
+ } finally {
1647
+ preflight.disconnect();
1648
+ }
1649
+ if (dotnsPreflight) {
1650
+ setDeployAttribute("deploy.dotns.preflight.action", dotnsPreflight.plannedAction);
1651
+ setDeployAttribute("deploy.dotns.preflight.classification", popStatusName(dotnsPreflight.classification.status));
1652
+ }
1653
+ const alreadyOwned = dotnsPreflight.plannedAction === "already-owned-by-us";
1654
+ const reqSuffix = alreadyOwned ? " (already owned, requirement not enforced)" : "";
1655
+ console.log(` DotNS: ${name}.dot requires ${popStatusName(dotnsPreflight.classification.status)}${reqSuffix}`);
1656
+ if (dotnsPreflight.canProceed) {
1657
+ const fromName = popStatusName(dotnsPreflight.userStatus);
1658
+ console.log(` Your PoP: ${fromName}`);
1659
+ console.log(` Domain: ${dotnsPreflight.plannedAction === "already-owned-by-us" ? "owned by you" : "available"}`);
1660
+ }
1661
+ if (!dotnsPreflight.canProceed) {
1662
+ throw new NonRetryableError(
1663
+ dotnsPreflight.reason ?? "DotNS preflight rejected the deploy; please check the label and signer."
1664
+ );
1665
+ }
1666
+ }
1667
+ provider = await reconnect();
1668
+ const providerWithReconnect = { ...provider, reconnect, bulletinAuthorizeV2: envBulletinAuthorizeV2 };
1669
+ const [isTestnet, estimated] = await Promise.all([
1670
+ detectTestnet(provider.unsafeApi),
1671
+ estimateUploadBytes(content)
1672
+ ]);
1673
+ setDeployAttribute("deploy.is_testnet", isTestnet ? "true" : "false");
1674
+ if (isTestnet && estimated != null) {
1675
+ const uploadBytes = Math.ceil(estimated * 1.2);
1676
+ const chunksNeeded = Math.max(1, Math.ceil(uploadBytes / CHUNK_SIZE));
1677
+ const needs = { txs: BigInt(chunksNeeded + 2), bytes: BigInt(uploadBytes) };
1678
+ await topUpBy(provider.unsafeApi, provider.ss58, needs, "uploader", envBulletinAuthorizeV2);
1679
+ }
1680
+ console.log("\n" + "=".repeat(60));
1681
+ console.log("Storage");
1682
+ console.log("=".repeat(60));
1683
+ setDeployAttribute("deploy.content_type", "unknown");
1684
+ setDeployAttribute("deploy.encrypted", "false");
1685
+ await withSpan("deploy.storage", "1. storage", {}, async () => {
1686
+ if (options.inputCar) {
1687
+ setDeployAttribute("deploy.content_type", "inputCar");
1688
+ const carPath = path.resolve(options.inputCar);
1689
+ if (!fs.existsSync(carPath)) throw new Error(`CAR file not found: ${carPath}`);
1690
+ console.log(`
1691
+ Mode: Pre-built CAR`);
1692
+ console.log(` Path: ${carPath}`);
1693
+ let carContent = fs.readFileSync(carPath);
1694
+ console.log(` Size: ${(carContent.length / 1024 / 1024).toFixed(2)} MB`);
1695
+ const reader = await CarReader.fromBytes(carContent);
1696
+ const roots = await reader.getRoots();
1697
+ if (roots.length === 0) throw new Error("CAR file has no roots");
1698
+ ipfsCid = roots[0].toString();
1699
+ console.log(` Root CID: ${ipfsCid}`);
1700
+ if (options.password) {
1701
+ setDeployAttribute("deploy.encrypted", "true");
1702
+ console.log(` Encrypting CAR file...`);
1703
+ carContent = await encryptContent(carContent, options.password);
1704
+ console.log(` Encrypted: ${(carContent.length / 1024 / 1024).toFixed(2)} MB`);
1705
+ }
1706
+ let carChunks;
1707
+ if (options.password) {
1708
+ carChunks = chunk(carContent, CHUNK_SIZE);
1709
+ } else {
1710
+ try {
1711
+ let prevStableOrder = [];
1712
+ const manifestBytes = await extractManifestFromCar(carContent);
1713
+ if (manifestBytes) {
1714
+ const parsed2 = parseManifest(Buffer.from(manifestBytes).toString("utf8"));
1715
+ if (parsed2.ok) prevStableOrder = parsed2.manifest.stableBlockOrder;
1716
+ }
1717
+ const rebuilt = await rebuildOrderedCarFromBytes(carContent, prevStableOrder);
1718
+ if (Buffer.compare(Buffer.from(rebuilt.carBytes), Buffer.from(carContent)) === 0) {
1719
+ carChunks = rebuilt.chunks;
1720
+ } else {
1721
+ captureWarning("input CAR ordered rechunk drift; falling back to size chunking", {
1722
+ rootCid: ipfsCid
1723
+ });
1724
+ carChunks = chunk(carContent, CHUNK_SIZE);
1725
+ }
1726
+ } catch (err) {
1727
+ captureWarning("input CAR ordered rechunk failed; falling back to size chunking", {
1728
+ rootCid: ipfsCid,
1729
+ reason: err?.message ?? String(err)
1730
+ });
1731
+ carChunks = chunk(carContent, CHUNK_SIZE);
1732
+ }
1733
+ }
1734
+ const predictedStorageCid = computeStorageCid(carChunks);
1735
+ if (options.ghPagesMirror) {
1736
+ mirrorPromise = mirrorToGitHubPages({
1737
+ domain: name,
1738
+ carBytes: carContent,
1739
+ cid: predictedStorageCid,
1740
+ toolVersion: VERSION,
1741
+ bulletinRpc: BULLETIN_ENDPOINTS[0],
1742
+ encrypted: Boolean(options.password)
1743
+ }).catch((err) => err instanceof Error ? err : new Error(String(err)));
1744
+ }
1745
+ cid = (await storeChunkedContent(carChunks, providerWithReconnect)).storageCid;
1746
+ } else if (process.env.IPFS_CID) {
1747
+ setDeployAttribute("deploy.content_type", "ipfsCid");
1748
+ if (options.password) {
1749
+ throw new Error(
1750
+ "IPFS_CID and --password are mutually exclusive: IPFS_CID skips the upload step, so there is nothing to encrypt. Either unset IPFS_CID to upload and encrypt fresh content, or remove --password to reuse the existing CID as-is."
1751
+ );
1752
+ }
1753
+ cid = process.env.IPFS_CID;
1754
+ ipfsCid = cid;
1755
+ console.log(`
1756
+ Using CID: ${cid}`);
1757
+ } else if (Array.isArray(content)) {
1758
+ setDeployAttribute("deploy.content_type", "multiChunk");
1759
+ console.log(`
1760
+ Mode: Multi-chunk (${content.length} chunks)`);
1761
+ let contentChunks = content;
1762
+ if (options.password) {
1763
+ setDeployAttribute("deploy.encrypted", "true");
1764
+ console.log(` Encrypting...`);
1765
+ const encrypted = await encryptContent(Buffer.concat(content), options.password);
1766
+ console.log(` Encrypted: ${(encrypted.length / 1024).toFixed(1)} KB`);
1767
+ contentChunks = chunk(encrypted);
1768
+ }
1769
+ cid = (await storeChunkedContent(contentChunks, providerWithReconnect)).storageCid;
1770
+ } else if (typeof content === "string") {
1771
+ setDeployAttribute("deploy.content_type", "path");
1772
+ const contentPath = path.resolve(content);
1773
+ if (!fs.existsSync(contentPath)) throw new Error(`Path not found: ${contentPath}`);
1774
+ const stats = fs.statSync(contentPath);
1775
+ if (stats.isDirectory()) {
1776
+ setDeployAttribute("deploy.content_type", "directory");
1777
+ console.log(`
1778
+ Mode: Directory`);
1779
+ console.log(` Path: ${contentPath}`);
1780
+ if (previousContenthashCid) console.log(` Incremental: previous contenthash ${previousContenthashCid}`);
1781
+ else console.log(` Incremental: first deploy (no previous contenthash)`);
1782
+ if (options.password) setDeployAttribute("deploy.encrypted", "true");
1783
+ const storeFn = options.password ? storeDirectory : storeDirectoryV2;
1784
+ const { storageCid: sCid, ipfsCid: iCid } = await storeFn(contentPath, {
1785
+ provider: providerWithReconnect,
1786
+ password: options.password,
1787
+ jsMerkle: options.jsMerkle,
1788
+ previousContenthash: previousContenthashCid,
1789
+ allowLargeDeploy: options.allowLargeDeploy,
1790
+ reproducibleSource: options.reproducibleSource,
1791
+ domain: name,
1792
+ gateway: envIpfs,
1793
+ dumpCar: options.dumpCar,
1794
+ onCarReady: (carBytes, predictedCid) => {
1795
+ if (options.ghPagesMirror) {
1796
+ mirrorPromise = mirrorToGitHubPages({
1797
+ domain: name,
1798
+ carBytes,
1799
+ cid: predictedCid,
1800
+ toolVersion: VERSION,
1801
+ bulletinRpc: BULLETIN_ENDPOINTS[0],
1802
+ encrypted: Boolean(options.password)
1803
+ }).catch((err) => err instanceof Error ? err : new Error(String(err)));
1804
+ }
1805
+ }
1806
+ });
1807
+ cid = sCid;
1808
+ ipfsCid = iCid;
1809
+ } else {
1810
+ setDeployAttribute("deploy.content_type", "file");
1811
+ console.log(`
1812
+ Mode: File`);
1813
+ console.log(` Path: ${contentPath}`);
1814
+ let fileContent = fs.readFileSync(contentPath);
1815
+ if (options.password) {
1816
+ setDeployAttribute("deploy.encrypted", "true");
1817
+ console.log(` Encrypting...`);
1818
+ fileContent = await encryptContent(fileContent, options.password);
1819
+ console.log(` Encrypted: ${(fileContent.length / 1024).toFixed(1)} KB`);
1820
+ }
1821
+ if (fileContent.length > MAX_FILE_SIZE) {
1822
+ console.log(` Exceeds 8MB, chunking...`);
1823
+ cid = (await storeChunkedContent(chunk(fileContent), providerWithReconnect)).storageCid;
1824
+ } else {
1825
+ cid = await storeFile(fileContent, providerWithReconnect);
1826
+ }
1827
+ }
1828
+ } else if (content instanceof Uint8Array) {
1829
+ setDeployAttribute("deploy.content_type", "multiChunk");
1830
+ console.log(`
1831
+ Mode: Bytes`);
1832
+ let bytesContent = content;
1833
+ if (options.password) {
1834
+ setDeployAttribute("deploy.encrypted", "true");
1835
+ console.log(` Encrypting...`);
1836
+ bytesContent = await encryptContent(bytesContent, options.password);
1837
+ console.log(` Encrypted: ${(bytesContent.length / 1024).toFixed(1)} KB`);
1838
+ }
1839
+ if (bytesContent.length > MAX_FILE_SIZE) {
1840
+ console.log(` Exceeds 8MB, chunking...`);
1841
+ cid = (await storeChunkedContent(chunk(bytesContent), providerWithReconnect)).storageCid;
1842
+ } else {
1843
+ cid = await storeFile(bytesContent, providerWithReconnect);
1844
+ }
1845
+ } else {
1846
+ throw new Error("Invalid content: must be path, Uint8Array, or Array<Uint8Array>");
1847
+ }
1848
+ });
1849
+ setDeployAttribute("deploy.cid", cid);
1850
+ if (options.attributes) {
1851
+ for (const [key, value] of Object.entries(options.attributes)) {
1852
+ setDeployAttribute(key, value);
1853
+ }
1854
+ }
1855
+ console.log("\n" + "=".repeat(60));
1856
+ console.log("DotNS");
1857
+ console.log("=".repeat(60));
1858
+ await withSpan("deploy.dotns", "2. dotns", { "deploy.domain": name, "deploy.subdomain": String(parsed?.isSubdomain ?? false) }, async () => {
1859
+ const dotns = new DotNS();
1860
+ await dotns.connect(resolveDotnsConnectOptions(options, envAssetHub, envAutoAccountMapping, envContracts, envNativeToEthRatio, envId, envPopSelfServe, envRegisterStorageDeposit));
1861
+ if (parsed?.isSubdomain) {
1862
+ const { owned, owner } = await dotns.checkSubdomainOwnership(parsed.sublabel, parsed.parentLabel);
1863
+ if (owned) {
1864
+ console.log(` Status: Already owned`);
1865
+ } else if (owner) {
1866
+ throw new Error(`Subdomain ${parsed.fullName} is owned by ${owner}, not ${dotns.evmAddress}`);
1867
+ } else {
1868
+ const parentOwnership = await dotns.checkOwnership(parsed.parentLabel);
1869
+ if (!parentOwnership.owned) throw new Error(`You must own ${parsed.parentLabel}.dot to register subdomains under it`);
1870
+ console.log(` Status: Registering subdomain...`);
1871
+ await dotns.registerSubdomain(parsed.sublabel, parsed.parentLabel);
1872
+ }
1873
+ } else {
1874
+ const { owned } = await dotns.checkOwnership(name);
1875
+ if (owned) {
1876
+ console.log(` Status: Already owned`);
1877
+ } else {
1878
+ console.log(` Status: Registering...`);
1879
+ await dotns.register(name);
1880
+ }
1881
+ }
1882
+ const contenthashHex = `0x${encodeContenthash(cid)}`;
1883
+ await dotns.setContenthash(name, contenthashHex);
1884
+ if (options.publish && parsed) {
1885
+ await publish(dotns, parsed, options.failOnPublishError);
1886
+ }
1887
+ dotns.disconnect();
1888
+ });
1889
+ if (options.ghPagesMirror) {
1890
+ console.log("\n" + "=".repeat(60));
1891
+ console.log("Final checks");
1892
+ console.log("=".repeat(60));
1893
+ await withSpan("deploy.gh-pages-mirror", "4. gh-pages-mirror", { "deploy.domain": name }, async () => {
1894
+ const mirror = await mirrorPromise;
1895
+ if (mirror === null) {
1896
+ console.log(" GitHub Pages mirror: skipped (only directory deploys produce a CAR suitable for mirroring).");
1897
+ return;
1898
+ }
1899
+ if (mirror instanceof MirrorSkipped) {
1900
+ console.log(` GitHub Pages mirror: skipped \u2014 ${mirror.message}`);
1901
+ return;
1902
+ }
1903
+ if (mirror instanceof Error) {
1904
+ console.log(` GitHub Pages mirror: failed (non-fatal) \u2014 ${mirror.message}`);
1905
+ captureWarning("gh-pages mirror failed", { error: mirror.message.slice(0, 200) });
1906
+ return;
1907
+ }
1908
+ console.log(` Mirror: ${mirror.url}`);
1909
+ console.log(` Manifest: https://${mirror.owner}.github.io/${mirror.repo}/${mirror.manifestPath}`);
1910
+ setDeployAttribute("deploy.gh_pages_url", mirror.url);
1911
+ process.stdout.write(" Verifying Pages serves this deploy's CAR... ");
1912
+ const freshness = await pollMirrorFreshness(mirror.url, cid, { timeoutMs: 3 * 60 * 1e3, intervalMs: 1e4 });
1913
+ if (freshness.verified) {
1914
+ console.log(`ok (${freshness.attempts} attempt${freshness.attempts === 1 ? "" : "s"}, ${(freshness.durationMs / 1e3).toFixed(0)}s).`);
1915
+ setDeployAttribute("deploy.gh_pages_freshness_verified", "true");
1916
+ } else {
1917
+ console.log(`timed out.`);
1918
+ console.log(` GitHub Pages last served cid=${freshness.lastCid ?? "n/a"} (expected ${cid}); it should catch up shortly. Non-fatal.`);
1919
+ setDeployAttribute("deploy.gh_pages_freshness_verified", "false");
1920
+ }
1921
+ });
1922
+ }
1923
+ console.log("\n" + "=".repeat(60));
1924
+ console.log("DEPLOYMENT COMPLETE!");
1925
+ console.log("=".repeat(60));
1926
+ console.log("\n\u{1F53A} Polkadot Triangle");
1927
+ console.log(` - Polkadot Desktop: ${name}.dot`);
1928
+ console.log(` - Polkadot Browser: https://${name}.dot.li`);
1929
+ console.log("\n" + "=".repeat(60) + "\n");
1930
+ return { domainName: name, fullDomain: `${name}.dot`, cid, ipfsCid };
1931
+ } finally {
1932
+ if (_deployRpcFailedOver) setDeployAttribute("deploy.rpc.failed_over", "true");
1933
+ provider?.client.destroy();
1934
+ }
1935
+ });
1936
+ }
1937
+
1938
+ // src/merkle.ts
1939
+ var CidPreservingBlockstore = class {
1940
+ data = /* @__PURE__ */ new Map();
1941
+ async put(cid, bytes) {
1942
+ this.data.set(cid.toString(), { cid, bytes });
1943
+ return cid;
1944
+ }
1945
+ *all() {
1946
+ yield* this.data.values();
1947
+ }
1948
+ clear() {
1949
+ this.data.clear();
1950
+ }
1951
+ };
1952
+ function* walkDirectoryLazy(dirPath, prefix = "") {
1953
+ let dirents;
1954
+ try {
1955
+ dirents = fs2.readdirSync(dirPath, { withFileTypes: true });
1956
+ } catch (err) {
1957
+ const code = err.code;
1958
+ if (code === "ENOENT") throw new Error(`Directory not found: ${dirPath}`);
1959
+ if (code === "ENOTDIR") throw new Error(`Not a directory: ${dirPath}`);
1960
+ throw err;
1961
+ }
1962
+ dirents.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
1963
+ for (const entry of dirents) {
1964
+ const fullPath = path2.join(dirPath, entry.name);
1965
+ const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
1966
+ if (entry.isDirectory()) {
1967
+ yield* walkDirectoryLazy(fullPath, relativePath);
1968
+ } else if (entry.isFile()) {
1969
+ yield { path: relativePath, absolutePath: fullPath };
1970
+ }
1971
+ }
1972
+ }
1973
+ async function collectBytes(iter) {
1974
+ const parts = [];
1975
+ let totalLength = 0;
1976
+ for await (const chunk2 of iter) {
1977
+ parts.push(chunk2);
1978
+ totalLength += chunk2.length;
1979
+ }
1980
+ const result = new Uint8Array(totalLength);
1981
+ let offset = 0;
1982
+ for (let i = 0; i < parts.length; i++) {
1983
+ const part = parts[i];
1984
+ result.set(part, offset);
1985
+ offset += part.length;
1986
+ parts[i] = void 0;
1987
+ }
1988
+ return result;
1989
+ }
1990
+ function walkFileBlocks(rootCidStr, blocks) {
1991
+ const fileBlocks = /* @__PURE__ */ new Map();
1992
+ const fileCids = /* @__PURE__ */ new Map();
1993
+ const rootBlockCids = [];
1994
+ const subdirCids = [];
1995
+ const subdirSeen = /* @__PURE__ */ new Set();
1996
+ walkNode(rootCidStr, "", "", blocks, fileBlocks, fileCids, rootBlockCids, subdirCids, subdirSeen, true);
1997
+ return { fileBlocks, fileCids, rootBlockCids, subdirCids };
1998
+ }
1999
+ function walkNode(cidStr, pathSoFar, fullDirPath, blocks, fileBlocks, fileCids, intermediates, subdirCids, subdirSeen, isRoot) {
2000
+ const cid = CID2.parse(cidStr);
2001
+ if (cid.code === 85) {
2002
+ if (pathSoFar) {
2003
+ fileBlocks.set(pathSoFar, [cidStr]);
2004
+ fileCids.set(pathSoFar, cidStr);
2005
+ }
2006
+ return;
2007
+ }
2008
+ if (cid.code !== 112) {
2009
+ if (pathSoFar) {
2010
+ fileBlocks.set(pathSoFar, [cidStr]);
2011
+ fileCids.set(pathSoFar, cidStr);
2012
+ }
2013
+ return;
2014
+ }
2015
+ const bytes = blocks.get(cidStr);
2016
+ if (!bytes) throw new Error(`block ${cidStr} not in block source`);
2017
+ intermediates.push(cidStr);
2018
+ const node = dagPB2.decode(bytes);
2019
+ let unixfs;
2020
+ if (node.Data) {
2021
+ try {
2022
+ unixfs = UnixFS2.unmarshal(node.Data);
2023
+ } catch {
2024
+ unixfs = void 0;
2025
+ }
2026
+ }
2027
+ if (unixfs && unixfs.isDirectory()) {
2028
+ if (!isRoot && !subdirSeen.has(cidStr)) {
2029
+ subdirCids.push(cidStr);
2030
+ subdirSeen.add(cidStr);
2031
+ }
2032
+ for (const link of node.Links ?? []) {
2033
+ const childName = link.Name ?? "";
2034
+ const childCid = link.Hash.toString();
2035
+ const childPath = pathSoFar ? `${pathSoFar}/${childName}` : childName;
2036
+ walkNode(childCid, childPath, childPath, blocks, fileBlocks, fileCids, intermediates, subdirCids, subdirSeen, false);
2037
+ }
2038
+ } else {
2039
+ const fileBlockList = [cidStr];
2040
+ for (const link of node.Links ?? []) {
2041
+ collectFileBlocks(link.Hash.toString(), blocks, fileBlockList);
2042
+ }
2043
+ if (pathSoFar) {
2044
+ fileBlocks.set(pathSoFar, fileBlockList);
2045
+ fileCids.set(pathSoFar, cidStr);
2046
+ }
2047
+ }
2048
+ }
2049
+ function collectFileBlocks(cidStr, blocks, fileBlockList) {
2050
+ const cid = CID2.parse(cidStr);
2051
+ if (cid.code === 85) {
2052
+ fileBlockList.push(cidStr);
2053
+ return;
2054
+ }
2055
+ if (cid.code !== 112) {
2056
+ fileBlockList.push(cidStr);
2057
+ return;
2058
+ }
2059
+ fileBlockList.push(cidStr);
2060
+ const bytes = blocks.get(cidStr);
2061
+ if (!bytes) return;
2062
+ const node = dagPB2.decode(bytes);
2063
+ for (const link of node.Links ?? []) {
2064
+ collectFileBlocks(link.Hash.toString(), blocks, fileBlockList);
2065
+ }
2066
+ }
2067
+ async function merkleizeJSBackend(directoryPath) {
2068
+ const blockstore = new CidPreservingBlockstore();
2069
+ const source = (function* () {
2070
+ for (const file of walkDirectoryLazy(directoryPath)) {
2071
+ yield {
2072
+ path: file.path,
2073
+ content: (async function* () {
2074
+ yield fs2.readFileSync(file.absolutePath);
2075
+ })()
2076
+ };
2077
+ }
2078
+ })();
2079
+ let rootCid;
2080
+ for await (const entry of importer(source, blockstore, {
2081
+ cidVersion: 1,
2082
+ rawLeaves: true,
2083
+ wrapWithDirectory: true
2084
+ })) {
2085
+ rootCid = entry.cid;
2086
+ }
2087
+ if (!rootCid) throw new Error("Merkleization produced no root CID");
2088
+ const blocks = /* @__PURE__ */ new Map();
2089
+ for (const { cid, bytes } of blockstore.all()) {
2090
+ blocks.set(cid.toString(), bytes);
2091
+ }
2092
+ blockstore.clear();
2093
+ const { fileBlocks, fileCids, rootBlockCids, subdirCids } = walkFileBlocks(rootCid.toString(), blocks);
2094
+ return { rootCid: rootCid.toString(), blocks, fileBlocks, fileCids, rootBlockCids, subdirCids };
2095
+ }
2096
+ async function merkleizeKuboBackend(directoryPath) {
2097
+ const carPath = path2.join(path2.dirname(directoryPath), `${path2.basename(directoryPath)}.car`);
2098
+ const cidStr = execSync2(
2099
+ `ipfs add -Q -r --hidden --cid-version=1 --raw-leaves --pin=false "${directoryPath}"`,
2100
+ { encoding: "utf-8" }
2101
+ ).trim();
2102
+ execSync2(`ipfs dag export ${cidStr} > "${carPath}"`);
2103
+ const carBytes = fs2.readFileSync(carPath);
2104
+ const reader = await CarReader2.fromBytes(carBytes);
2105
+ const blocks = /* @__PURE__ */ new Map();
2106
+ for await (const { cid, bytes } of reader.blocks()) {
2107
+ blocks.set(cid.toString(), bytes);
2108
+ }
2109
+ const { fileBlocks, fileCids, rootBlockCids, subdirCids } = walkFileBlocks(cidStr, blocks);
2110
+ return { rootCid: cidStr, blocks, fileBlocks, fileCids, rootBlockCids, subdirCids };
2111
+ }
2112
+ async function merkleizeBackend(directoryPath, useKubo) {
2113
+ if (useKubo) {
2114
+ console.log(` Merkleizing (Kubo): ${directoryPath}`);
2115
+ return merkleizeKuboBackend(directoryPath);
2116
+ }
2117
+ console.log(` Merkleizing (JS): ${directoryPath}`);
2118
+ return merkleizeJSBackend(directoryPath);
2119
+ }
2120
+ function encodeCarFrame(cid, payload) {
2121
+ const cidBytes = cid.bytes;
2122
+ const innerLen = cidBytes.length + payload.length;
2123
+ const lenBytes = encodeVarint(innerLen);
2124
+ const out = new Uint8Array(lenBytes.length + cidBytes.length + payload.length);
2125
+ out.set(lenBytes, 0);
2126
+ out.set(cidBytes, lenBytes.length);
2127
+ out.set(payload, lenBytes.length + cidBytes.length);
2128
+ return out;
2129
+ }
2130
+ function encodeVarint(n) {
2131
+ const out = [];
2132
+ while (n > 127) {
2133
+ out.push(n & 127 | 128);
2134
+ n >>>= 7;
2135
+ }
2136
+ out.push(n & 127);
2137
+ return new Uint8Array(out);
2138
+ }
2139
+ function concatBytes(parts) {
2140
+ let total = 0;
2141
+ for (const p of parts) total += p.length;
2142
+ const out = new Uint8Array(total);
2143
+ let off = 0;
2144
+ for (const p of parts) {
2145
+ out.set(p, off);
2146
+ off += p.length;
2147
+ }
2148
+ return out;
2149
+ }
2150
+ async function encodeCarHeader(root) {
2151
+ const { writer, out } = CarWriter.create([root]);
2152
+ const headerPromise = collectBytes(out);
2153
+ await writer.close();
2154
+ return await headerPromise;
2155
+ }
2156
+ function frameBlockCid(frame) {
2157
+ let offset = 0;
2158
+ while (offset < frame.length && frame[offset] & 128) offset++;
2159
+ offset++;
2160
+ const [cid] = CID2.decodeFirst(frame.slice(offset));
2161
+ return cid.toString();
2162
+ }
2163
+ async function buildOrderedCar(options) {
2164
+ const { output, prevStableOrder = [] } = options;
2165
+ const clsFn = options.classifyFn ?? ((p) => classifyFile(p));
2166
+ const MANIFEST_PATH_LITERAL = ".bulletin-deploy/manifest.json";
2167
+ const stableFiles = [];
2168
+ const volatileFiles = [];
2169
+ for (const [filePath, fileBlockCids] of output.fileBlocks) {
2170
+ if (filePath === MANIFEST_PATH_LITERAL) continue;
2171
+ const fileCid = output.fileCids.get(filePath);
2172
+ const frames = [];
2173
+ for (const blockCid of fileBlockCids) {
2174
+ const blockBytes = output.blocks.get(blockCid);
2175
+ if (!blockBytes) throw new Error(`buildOrderedCar: block ${blockCid} missing`);
2176
+ frames.push(encodeCarFrame(CID2.parse(blockCid), blockBytes));
2177
+ }
2178
+ const size = frames.reduce((s, b) => s + b.length, 0);
2179
+ const cls = clsFn(filePath, fileCid);
2180
+ (cls === "stable" ? stableFiles : volatileFiles).push({ path: filePath, fileCid, blocks: frames, size });
2181
+ }
2182
+ const fileByCid = new Map(stableFiles.map((f) => [f.fileCid, f]));
2183
+ const placed = /* @__PURE__ */ new Set();
2184
+ const section1Files = [];
2185
+ for (const cid of prevStableOrder) {
2186
+ const f = fileByCid.get(cid);
2187
+ if (f && !placed.has(cid)) {
2188
+ section1Files.push(f);
2189
+ placed.add(cid);
2190
+ }
2191
+ }
2192
+ const newStable = stableFiles.filter((f) => !placed.has(f.fileCid));
2193
+ newStable.sort(
2194
+ (a, b) => b.size - a.size || (a.fileCid < b.fileCid ? -1 : a.fileCid > b.fileCid ? 1 : 0)
2195
+ );
2196
+ for (const f of newStable) section1Files.push(f);
2197
+ volatileFiles.sort(
2198
+ (a, b) => b.size - a.size || (a.fileCid < b.fileCid ? -1 : a.fileCid > b.fileCid ? 1 : 0)
2199
+ );
2200
+ const rootCidParsed = CID2.parse(output.rootCid);
2201
+ const headerBytes = await encodeCarHeader(rootCidParsed);
2202
+ const section0Frames = [headerBytes];
2203
+ const manifestBlockCids = output.fileBlocks.get(MANIFEST_PATH_LITERAL) ?? [];
2204
+ if (manifestBlockCids.length > 0) {
2205
+ for (const blockCid of manifestBlockCids) {
2206
+ const blockBytes = output.blocks.get(blockCid);
2207
+ if (!blockBytes) throw new Error(`buildOrderedCar: manifest block ${blockCid} missing`);
2208
+ section0Frames.push(encodeCarFrame(CID2.parse(blockCid), blockBytes));
2209
+ }
2210
+ }
2211
+ const section0Bytes = concatBytes(section0Frames);
2212
+ const rootDirBytes = output.blocks.get(output.rootCid);
2213
+ if (!rootDirBytes) throw new Error(`buildOrderedCar: root block ${output.rootCid} missing`);
2214
+ const section2LeadFrames = [encodeCarFrame(rootCidParsed, rootDirBytes)];
2215
+ for (const cid of output.subdirCids) {
2216
+ const bytes = output.blocks.get(cid);
2217
+ if (!bytes) continue;
2218
+ section2LeadFrames.push(encodeCarFrame(CID2.parse(cid), bytes));
2219
+ }
2220
+ const section1SectionFiles = section1Files.map((f) => ({ blocks: f.blocks }));
2221
+ const section2SectionFiles = [
2222
+ { blocks: section2LeadFrames },
2223
+ ...volatileFiles.map((f) => ({ blocks: f.blocks }))
2224
+ ];
2225
+ const section1Chunks = packSection(section1SectionFiles);
2226
+ const section2Chunks = packSection(section2SectionFiles);
2227
+ const section0Chunks = section0Bytes.length > 0 ? [section0Bytes] : [];
2228
+ const allChunks = [...section0Chunks, ...section1Chunks, ...section2Chunks];
2229
+ const carBytes = concatBytes(allChunks);
2230
+ const chunkCids = allChunks.map((b) => createCID(b).toString());
2231
+ const section1ChunkCids = section1Chunks.map((b) => createCID(b).toString());
2232
+ const blockOrder = [];
2233
+ const collectFrameCids = (frames) => {
2234
+ for (const frame of frames) {
2235
+ if (frame === headerBytes) continue;
2236
+ try {
2237
+ blockOrder.push(frameBlockCid(frame));
2238
+ } catch {
2239
+ }
2240
+ }
2241
+ };
2242
+ collectFrameCids(section0Frames);
2243
+ for (const f of section1Files) collectFrameCids(f.blocks);
2244
+ collectFrameCids(section2LeadFrames);
2245
+ for (const f of volatileFiles) collectFrameCids(f.blocks);
2246
+ const stableOrder = section1Files.map((f) => f.fileCid);
2247
+ const s1Bytes = section1Chunks.reduce((s, b) => s + b.length, 0);
2248
+ const s2Bytes = section2Chunks.reduce((s, b) => s + b.length, 0);
2249
+ console.log(
2250
+ ` CAR (3-section): ${(carBytes.length / 1024 / 1024).toFixed(2)} MB (s0=${section0Bytes.length}B s1=${s1Bytes}B s2=${s2Bytes}B), ${allChunks.length} chunks (${section0Chunks.length}+${section1Chunks.length}+${section2Chunks.length})`
2251
+ );
2252
+ return {
2253
+ carBytes,
2254
+ cid: output.rootCid,
2255
+ blockOrder,
2256
+ stableOrder,
2257
+ chunks: allChunks,
2258
+ chunkCids,
2259
+ section1ChunkCids,
2260
+ sectionSizes: {
2261
+ section0: section0Bytes.length,
2262
+ section1: s1Bytes,
2263
+ section2: s2Bytes
2264
+ },
2265
+ sectionChunkCounts: { section0: section0Chunks.length, section1: section1Chunks.length, section2: section2Chunks.length },
2266
+ blocks: output.blocks,
2267
+ fileCids: output.fileCids
2268
+ };
2269
+ }
2270
+ async function rebuildOrderedCarFromBytes(carBytes, prevStableOrder = []) {
2271
+ const reader = await CarReader2.fromBytes(carBytes);
2272
+ const roots = await reader.getRoots();
2273
+ if (roots.length === 0) throw new Error("CAR file has no roots");
2274
+ const blocks = /* @__PURE__ */ new Map();
2275
+ for await (const { cid, bytes } of reader.blocks()) {
2276
+ blocks.set(cid.toString(), bytes);
2277
+ }
2278
+ const rootCid = roots[0].toString();
2279
+ const { fileBlocks, fileCids, rootBlockCids, subdirCids } = walkFileBlocks(rootCid, blocks);
2280
+ return buildOrderedCar({
2281
+ output: { rootCid, blocks, fileBlocks, fileCids, rootBlockCids, subdirCids },
2282
+ prevStableOrder
2283
+ });
2284
+ }
2285
+ async function merkleizeWithStableOrder(directoryPath, prevStableOrder, options) {
2286
+ const useKubo = options?.useKubo ?? false;
2287
+ const output = await merkleizeBackend(directoryPath, useKubo);
2288
+ return buildOrderedCar({ output, classifyFn: options?.classifyFn, prevStableOrder });
2289
+ }
2290
+ async function merkleizeJS(directoryPath) {
2291
+ console.log(` Merkleizing (JS): ${directoryPath}`);
2292
+ const blockstore = new CidPreservingBlockstore();
2293
+ const source = (function* () {
2294
+ for (const file of walkDirectoryLazy(directoryPath)) {
2295
+ yield {
2296
+ path: file.path,
2297
+ content: (async function* () {
2298
+ yield fs2.readFileSync(file.absolutePath);
2299
+ })()
2300
+ };
2301
+ }
2302
+ })();
2303
+ let rootCid;
2304
+ for await (const entry of importer(source, blockstore, {
2305
+ cidVersion: 1,
2306
+ rawLeaves: true,
2307
+ wrapWithDirectory: true
2308
+ })) {
2309
+ rootCid = entry.cid;
2310
+ }
2311
+ if (!rootCid) throw new Error("Merkleization produced no root CID");
2312
+ const { writer, out } = CarWriter.create([rootCid]);
2313
+ const collectPromise = collectBytes(out);
2314
+ for (const { cid, bytes } of blockstore.all()) {
2315
+ await writer.put({ cid, bytes });
2316
+ }
2317
+ await writer.close();
2318
+ const carBytes = await collectPromise;
2319
+ blockstore.clear();
2320
+ console.log(` CAR (JS): ${(carBytes.length / 1024 / 1024).toFixed(2)} MB`);
2321
+ return { carBytes, cid: rootCid.toString() };
2322
+ }
2323
+
2324
+ export {
2325
+ merkleizeJSBackend,
2326
+ merkleizeKuboBackend,
2327
+ merkleizeBackend,
2328
+ buildOrderedCar,
2329
+ rebuildOrderedCarFromBytes,
2330
+ merkleizeWithStableOrder,
2331
+ merkleizeJS,
2332
+ friendlyChainError,
2333
+ DEFAULT_BULLETIN_RPC,
2334
+ DEFAULT_POOL_SIZE,
2335
+ setWsHaltCallback,
2336
+ CHUNK_MORTALITY_PERIOD,
2337
+ retryBudgetExhausted,
2338
+ isConnectionError,
2339
+ deriveRootSigner,
2340
+ createCID,
2341
+ encodeContenthash,
2342
+ ENCRYPT_MAGIC,
2343
+ ENCRYPT_SALT_LEN,
2344
+ ENCRYPT_NONCE_LEN,
2345
+ ENCRYPT_TAG_LEN,
2346
+ ENCRYPT_KEY_LEN,
2347
+ ENCRYPT_PBKDF2_ITERATIONS,
2348
+ encryptContent,
2349
+ storeFile,
2350
+ __assignDenseNoncesForTest,
2351
+ storeChunkedContent,
2352
+ chunk,
2353
+ hasIPFS,
2354
+ merkleize,
2355
+ computeStorageCid,
2356
+ storeDirectory,
2357
+ buildFilesMap,
2358
+ detectFramework,
2359
+ checkDeploySize,
2360
+ resolveReproducibleTimestamp,
2361
+ applyManifestFetchAttributes,
2362
+ storeDirectoryV2,
2363
+ resolveDotnsConnectOptions,
2364
+ estimateUploadBytes,
2365
+ assertSubdomainOwnerMatchesSigner,
2366
+ unpublish,
2367
+ deploy
2368
+ };