bulletin-deploy 0.6.3 → 0.6.5-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.
@@ -4,13 +4,13 @@ import {
4
4
  TX_TIMEOUT_MS,
5
5
  fetchNonce,
6
6
  validateDomainLabel
7
- } from "./chunk-TITCVJJF.js";
7
+ } from "./chunk-NXITLUZT.js";
8
8
  import {
9
9
  derivePoolAccounts,
10
10
  ensureAuthorized,
11
11
  fetchPoolAuthorizations,
12
12
  selectAccount
13
- } from "./chunk-AIHW2WLO.js";
13
+ } from "./chunk-LGPTJYA3.js";
14
14
  import {
15
15
  VERSION,
16
16
  captureWarning,
@@ -18,7 +18,7 @@ import {
18
18
  setDeployAttribute,
19
19
  withDeploySpan,
20
20
  withSpan
21
- } from "./chunk-ECOC6TV4.js";
21
+ } from "./chunk-FHAEFLVV.js";
22
22
 
23
23
  // src/deploy.ts
24
24
  import { Buffer } from "buffer";
@@ -280,8 +280,10 @@ var BULLETIN_RPC = DEFAULT_BULLETIN_RPC;
280
280
  var POOL_SIZE = DEFAULT_POOL_SIZE;
281
281
  var CHUNK_SIZE = 1 * 1024 * 1024;
282
282
  var MAX_FILE_SIZE = 8 * 1024 * 1024;
283
- var MAX_RECONNECTIONS = parseInt(process.env.BULLETIN_MAX_RECONNECTIONS ?? "1", 10);
284
- var CHUNK_TIMEOUT_MS = 3e4;
283
+ var MAX_RECONNECTIONS = parseInt(process.env.BULLETIN_MAX_RECONNECTIONS ?? "3", 10);
284
+ var CHUNK_TIMEOUT_MS = parseInt(process.env.BULLETIN_CHUNK_TIMEOUT_MS ?? "60000", 10);
285
+ var RETRY_BASE_DELAY_MS = 2e3;
286
+ var RETRY_MAX_DELAY_MS = 15e3;
285
287
  function isConnectionError(error) {
286
288
  const msg = error?.message || String(error);
287
289
  return /heartbeat timeout|WS halt|Unable to connect/i.test(msg);
@@ -362,7 +364,7 @@ function toHashingEnum(mhCode) {
362
364
  }
363
365
  async function getProvider() {
364
366
  console.log(` Connecting to Bulletin: ${BULLETIN_RPC}`);
365
- const client = createPolkadotClient(withPolkadotSdkCompat(getWsProvider(BULLETIN_RPC)));
367
+ const client = createPolkadotClient(withPolkadotSdkCompat(getWsProvider([BULLETIN_RPC])));
366
368
  const unsafeApi = client.getUnsafeApi();
367
369
  try {
368
370
  await cryptoWaitReady();
@@ -373,10 +375,10 @@ async function getProvider() {
373
375
  if (!selected) {
374
376
  const best = authorizations.reduce((a, b) => a.transactions > b.transactions ? a : b);
375
377
  console.log(` All pool accounts low on capacity, auto-authorizing account ${best.index}...`);
376
- await ensureAuthorized(unsafeApi, best, BULLETIN_RPC);
378
+ await ensureAuthorized(unsafeApi, best.address, BULLETIN_RPC, `pool account ${best.index}`);
377
379
  selected = best;
378
380
  } else {
379
- await ensureAuthorized(unsafeApi, selected, BULLETIN_RPC);
381
+ await ensureAuthorized(unsafeApi, selected.address, BULLETIN_RPC, `pool account ${selected.index}`);
380
382
  }
381
383
  console.log(` Using pool account ${selected.index}: ${selected.address}`);
382
384
  setDeployAttribute("deploy.signer.mode", "pool");
@@ -390,7 +392,7 @@ async function getProvider() {
390
392
  }
391
393
  async function getDirectProvider(mnemonic) {
392
394
  console.log(` Connecting to Bulletin: ${BULLETIN_RPC}`);
393
- const client = createPolkadotClient(withPolkadotSdkCompat(getWsProvider(BULLETIN_RPC)));
395
+ const client = createPolkadotClient(withPolkadotSdkCompat(getWsProvider([BULLETIN_RPC])));
394
396
  const unsafeApi = client.getUnsafeApi();
395
397
  const { signer, ss58 } = deriveRootSigner(mnemonic);
396
398
  console.log(` Using direct signer: ${ss58}`);
@@ -401,46 +403,53 @@ async function getDirectProvider(mnemonic) {
401
403
  const bytesRemaining = auth ? auth.extent.bytes : 0n;
402
404
  if (txsRemaining === 0n && bytesRemaining === 0n) {
403
405
  client.destroy();
404
- throw new Error(`Account ${ss58} is not authorized for Bulletin storage. Run 'bulletin-deploy --bootstrap --mnemonic "..."' or authorize the account on-chain first.`);
406
+ throw new Error(`Account ${ss58} is not authorized for Bulletin storage.`);
405
407
  }
406
408
  console.log(` Authorization: ${txsRemaining} txs, ${Number(bytesRemaining) / 1e6}MB remaining`);
407
409
  setDeployAttribute("deploy.signer.mode", "direct");
408
410
  setDeployAttribute("deploy.signer.address", ss58);
409
411
  return { client, unsafeApi, signer, ss58 };
410
412
  }
413
+ var MAX_BEST_CHAIN_DROPS = 5;
411
414
  function watchTransaction(tx, signer, txOpts, onSuccess, { label = "transaction", rpc, senderSS58, expectedNonce, timeoutMs } = {}) {
412
415
  const timeout = timeoutMs ?? TX_TIMEOUT_MS;
413
416
  return new Promise((resolve2, reject) => {
414
417
  let settled = false;
418
+ let sub;
419
+ let dropCount = 0;
415
420
  const settle = (fn) => (...args) => {
416
- if (!settled) {
417
- settled = true;
418
- clearTimeout(timer);
419
- try {
420
- sub.unsubscribe();
421
- } catch {
421
+ if (settled) return;
422
+ settled = true;
423
+ clearTimeout(timer);
424
+ try {
425
+ sub?.unsubscribe();
426
+ } catch {
427
+ }
428
+ fn(...args);
429
+ };
430
+ const tryNonceFallback = async () => {
431
+ if (!rpc || !senderSS58 || expectedNonce == null) return false;
432
+ try {
433
+ const currentNonce = await fetchNonce(rpc, senderSS58);
434
+ if (settled) return true;
435
+ if (currentNonce > expectedNonce) {
436
+ console.log(` ${label}: nonce advanced (${expectedNonce} -> ${currentNonce}), tx was included`);
437
+ settle(resolve2)({ value: onSuccess(), viaFallback: true });
438
+ return true;
422
439
  }
423
- fn(...args);
440
+ } catch (e) {
441
+ if (settled) return true;
442
+ console.log(` ${label}: nonce fallback failed: ${e.message?.slice(0, 80)}`);
424
443
  }
444
+ return false;
425
445
  };
426
446
  const timer = setTimeout(async () => {
427
447
  if (settled) return;
428
- if (rpc && senderSS58 && expectedNonce != null) {
429
- try {
430
- const currentNonce = await fetchNonce(rpc, senderSS58);
431
- if (currentNonce > expectedNonce) {
432
- console.log(` ${label}: subscription timed out but nonce advanced (${expectedNonce} -> ${currentNonce}), tx was included`);
433
- settle(resolve2)({ value: onSuccess(), viaFallback: true });
434
- return;
435
- }
436
- } catch (e) {
437
- console.log(` ${label}: nonce check failed: ${e.message}`);
438
- }
439
- }
448
+ if (await tryNonceFallback()) return;
440
449
  settle(reject)(new Error(`${label} timed out after ${timeout / 1e3}s waiting for block confirmation`));
441
450
  }, timeout);
442
- const sub = tx.signSubmitAndWatch(signer, txOpts).subscribe({
443
- next: (event) => {
451
+ sub = tx.signSubmitAndWatch(signer, txOpts).subscribe({
452
+ next: async (event) => {
444
453
  if (event.type === "txBestBlocksState") {
445
454
  if (event.found) {
446
455
  if (event.ok) {
@@ -449,7 +458,14 @@ function watchTransaction(tx, signer, txOpts, onSuccess, { label = "transaction"
449
458
  settle(reject)(new Error(`${label} dispatch error`));
450
459
  }
451
460
  } else {
452
- console.log(` ${label}: tx dropped from best chain, waiting for re-inclusion...`);
461
+ dropCount++;
462
+ if (dropCount >= MAX_BEST_CHAIN_DROPS) {
463
+ console.log(` ${label}: tx dropped ${dropCount} times, checking nonce...`);
464
+ if (await tryNonceFallback()) return;
465
+ settle(reject)(new Error(`${label} tx dropped from best chain ${dropCount} times`));
466
+ } else {
467
+ console.log(` ${label}: tx dropped from best chain (${dropCount}/${MAX_BEST_CHAIN_DROPS}), waiting...`);
468
+ }
453
469
  }
454
470
  }
455
471
  },
@@ -525,19 +541,38 @@ async function storeChunkedContent(chunks, { client: existingClient, unsafeApi:
525
541
  ss58 = provider.ss58;
526
542
  ownsClient = true;
527
543
  }
544
+ const requiredTxs = BigInt(chunks.length + 1);
545
+ const auth = await unsafeApi.query.TransactionStorage.Authorizations.getValue(
546
+ Enum("Account", ss58)
547
+ );
548
+ const txsRemaining = auth ? BigInt(auth.extent.transactions) : 0n;
549
+ const bytesRemaining = auth ? auth.extent.bytes : 0n;
550
+ if (txsRemaining < requiredTxs || bytesRemaining < BigInt(totalBytes)) {
551
+ console.log(`
552
+ Account has insufficient authorization for this upload (need ${requiredTxs} txs / ${(totalBytes / 1e6).toFixed(1)}MB, have ${txsRemaining} txs / ${Number(bytesRemaining) / 1e6}MB)`);
553
+ console.log(` Attempting to re-authorize with Alice...`);
554
+ try {
555
+ await ensureAuthorized(unsafeApi, ss58, BULLETIN_RPC);
556
+ console.log(` Re-authorization successful`);
557
+ } catch (e) {
558
+ throw new Error(`Re-authorization failed: ${e.message}`);
559
+ }
560
+ }
528
561
  let reconnectionsUsed = 0;
529
562
  async function doReconnect() {
530
563
  if (!reconnect || reconnectionsUsed >= MAX_RECONNECTIONS) {
531
564
  throw new Error(`Connection lost and max reconnections (${MAX_RECONNECTIONS}) exhausted`);
532
565
  }
533
566
  reconnectionsUsed++;
567
+ const delay = Math.min(RETRY_BASE_DELAY_MS * Math.pow(2, reconnectionsUsed - 1), RETRY_MAX_DELAY_MS);
534
568
  console.log(`
535
- Connection lost, reconnecting to Bulletin (${reconnectionsUsed}/${MAX_RECONNECTIONS})...`);
569
+ Connection lost, reconnecting to Bulletin in ${(delay / 1e3).toFixed(0)}s (${reconnectionsUsed}/${MAX_RECONNECTIONS})...`);
536
570
  captureWarning("WebSocket connection lost, reconnecting", { reconnection: reconnectionsUsed, maxReconnections: MAX_RECONNECTIONS });
537
571
  try {
538
572
  client.destroy();
539
573
  } catch {
540
574
  }
575
+ await new Promise((r) => setTimeout(r, delay));
541
576
  const fresh = await reconnect();
542
577
  client = fresh.client;
543
578
  unsafeApi = fresh.unsafeApi;
@@ -589,10 +624,19 @@ async function storeChunkedContent(chunks, { client: existingClient, unsafeApi:
589
624
  captureWarning("Chunk upload failed, retrying", { chunkIndex: fail.index + 1, maxRetries: MAX_CHUNK_RETRIES, error: fail.error?.message?.slice(0, 200) });
590
625
  let retried = false;
591
626
  for (let attempt = 1; attempt <= MAX_CHUNK_RETRIES; attempt++) {
592
- console.log(` Retrying chunk ${fail.index + 1} (attempt ${attempt}/${MAX_CHUNK_RETRIES})...`);
593
- await new Promise((r) => setTimeout(r, 6e3));
594
- const freshNonce = await fetchNonce(BULLETIN_RPC, ss58);
627
+ const retryDelay = Math.min(RETRY_BASE_DELAY_MS * Math.pow(2, attempt - 1), RETRY_MAX_DELAY_MS);
628
+ console.log(` Retrying chunk ${fail.index + 1} (attempt ${attempt}/${MAX_CHUNK_RETRIES}) in ${(retryDelay / 1e3).toFixed(0)}s...`);
629
+ await new Promise((r) => setTimeout(r, retryDelay));
630
+ if (isConnectionError(fail.error) && reconnect && reconnectionsUsed < MAX_RECONNECTIONS) {
631
+ try {
632
+ await doReconnect();
633
+ } catch (reconnectErr) {
634
+ console.log(` Reconnect failed: ${reconnectErr.message?.slice(0, 80)}`);
635
+ break;
636
+ }
637
+ }
595
638
  try {
639
+ const freshNonce = await fetchNonce(BULLETIN_RPC, ss58);
596
640
  const result2 = await storeChunk(unsafeApi, signer, fail.chunkData, freshNonce, ss58);
597
641
  stored[fail.index] = result2;
598
642
  retried = true;
@@ -600,6 +644,12 @@ async function storeChunkedContent(chunks, { client: existingClient, unsafeApi:
600
644
  } catch (e) {
601
645
  captureWarning("Chunk retry failed", { chunkIndex: fail.index + 1, attempt, maxRetries: MAX_CHUNK_RETRIES, error: e.message?.slice(0, 200) });
602
646
  console.log(` Retry ${attempt} failed: ${e.message?.slice(0, 80)}`);
647
+ if (isConnectionError(e) && reconnect && reconnectionsUsed < MAX_RECONNECTIONS) {
648
+ try {
649
+ await doReconnect();
650
+ } catch {
651
+ }
652
+ }
603
653
  }
604
654
  }
605
655
  if (!retried) {
@@ -6,7 +6,7 @@ import * as path from "path";
6
6
  // package.json
7
7
  var package_default = {
8
8
  name: "bulletin-deploy",
9
- version: "0.6.3",
9
+ version: "0.6.5-rc.0",
10
10
  private: false,
11
11
  repository: {
12
12
  type: "git",
@@ -87,7 +87,7 @@ function extractRepoSlug(url) {
87
87
  }
88
88
  function tryGitRemote() {
89
89
  try {
90
- return extractRepoSlug(execSync("git remote get-url origin", { encoding: "utf-8" }).trim());
90
+ return extractRepoSlug(execSync("git remote get-url origin", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim());
91
91
  } catch {
92
92
  return void 0;
93
93
  }
@@ -106,7 +106,7 @@ function resolveRepo(domain) {
106
106
  }
107
107
  function tryGitBranch() {
108
108
  try {
109
- return execSync("git rev-parse --abbrev-ref HEAD", { encoding: "utf-8" }).trim();
109
+ return execSync("git rev-parse --abbrev-ref HEAD", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
110
110
  } catch {
111
111
  return "unknown";
112
112
  }
@@ -55,16 +55,16 @@ async function fetchPoolAuthorizations(api, accounts) {
55
55
  );
56
56
  return results;
57
57
  }
58
- async function ensureAuthorized(api, poolAccount, bulletinRpc) {
58
+ async function ensureAuthorized(api, address, bulletinRpc, label) {
59
59
  const auth = await api.query.TransactionStorage.Authorizations.getValue(
60
- Enum("Account", poolAccount.address)
60
+ Enum("Account", address)
61
61
  );
62
62
  const txsRemaining = auth ? BigInt(auth.extent.transactions) : 0n;
63
63
  const bytesRemaining = auth ? auth.extent.bytes : 0n;
64
64
  if (txsRemaining >= TOPUP_THRESHOLD_TXS && bytesRemaining >= TOPUP_THRESHOLD_BYTES) {
65
65
  return;
66
66
  }
67
- console.log(` Auto-authorizing pool account ${poolAccount.index} (${poolAccount.address.slice(0, 8)}...)...`);
67
+ console.log(` Auto-authorizing ${label ?? "account"} (${address.slice(0, 8)}...)...`);
68
68
  const aliceClient = createClient(withPolkadotSdkCompat(getWsProvider(bulletinRpc)));
69
69
  const aliceApi = aliceClient.getUnsafeApi();
70
70
  try {
@@ -72,7 +72,7 @@ async function ensureAuthorized(api, poolAccount, bulletinRpc) {
72
72
  const alice = keyring.addFromUri("//Alice");
73
73
  const aliceSigner = getPolkadotSigner(alice.publicKey, "Sr25519", (data) => alice.sign(data));
74
74
  const tx = aliceApi.tx.TransactionStorage.authorize_account({
75
- who: poolAccount.address,
75
+ who: address,
76
76
  transactions: TOPUP_TRANSACTIONS,
77
77
  bytes: TOPUP_BYTES
78
78
  });
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  captureWarning,
3
3
  withSpan
4
- } from "./chunk-ECOC6TV4.js";
4
+ } from "./chunk-FHAEFLVV.js";
5
5
 
6
6
  // src/dotns.ts
7
7
  import crypto from "crypto";
@@ -51,13 +51,18 @@ var _rpcIdCounter = 0;
51
51
  async function fetchNonce(rpc, ss58Address) {
52
52
  const WS = globalThis.WebSocket ?? (await import("./wrapper-IFSKR7DG.js")).default;
53
53
  return new Promise((resolve, reject) => {
54
- const timeout = setTimeout(() => {
54
+ let done = false;
55
+ const settle = (fn, ...args) => {
56
+ if (done) return;
57
+ done = true;
58
+ clearTimeout(timer);
55
59
  try {
56
60
  ws.close();
57
61
  } catch {
58
62
  }
59
- reject(new Error(`fetchNonce timed out after 10s for ${rpc}`));
60
- }, 1e4);
63
+ fn(...args);
64
+ };
65
+ const timer = setTimeout(() => settle(reject, new Error(`fetchNonce timed out after 8s for ${rpc}`)), 8e3);
61
66
  const ws = new WS(rpc);
62
67
  const id = ++_rpcIdCounter;
63
68
  ws.onopen = () => ws.send(JSON.stringify({ jsonrpc: "2.0", id, method: "system_accountNextIndex", params: [ss58Address] }));
@@ -65,16 +70,11 @@ async function fetchNonce(rpc, ss58Address) {
65
70
  const d = typeof e.data === "string" ? e.data : e.data.toString();
66
71
  const r = JSON.parse(d);
67
72
  if (r.id === id) {
68
- clearTimeout(timeout);
69
- ws.close();
70
- r.error ? reject(new Error(r.error.message)) : resolve(r.result);
73
+ r.error ? settle(reject, new Error(r.error.message)) : settle(resolve, r.result);
71
74
  }
72
75
  };
73
- ws.onerror = () => {
74
- clearTimeout(timeout);
75
- ws.close();
76
- reject(new Error(`WebSocket to ${rpc} failed`));
77
- };
76
+ ws.onerror = () => settle(reject, new Error(`WebSocket to ${rpc} failed`));
77
+ ws.onclose = () => settle(reject, new Error(`WebSocket to ${rpc} closed before response`));
78
78
  });
79
79
  }
80
80
  var ProofOfPersonhoodStatus = {
package/dist/deploy.js CHANGED
@@ -19,10 +19,10 @@ import {
19
19
  storeChunkedContent,
20
20
  storeDirectory,
21
21
  storeFile
22
- } from "./chunk-IPTNRVHY.js";
23
- import "./chunk-TITCVJJF.js";
24
- import "./chunk-AIHW2WLO.js";
25
- import "./chunk-ECOC6TV4.js";
22
+ } from "./chunk-FBV763OM.js";
23
+ import "./chunk-NXITLUZT.js";
24
+ import "./chunk-LGPTJYA3.js";
25
+ import "./chunk-FHAEFLVV.js";
26
26
  import "./chunk-QGM4M3NI.js";
27
27
  export {
28
28
  DEFAULT_BULLETIN_RPC,
package/dist/dotns.js CHANGED
@@ -19,8 +19,8 @@ import {
19
19
  sanitizeDomainLabel,
20
20
  stripTrailingDigits,
21
21
  validateDomainLabel
22
- } from "./chunk-TITCVJJF.js";
23
- import "./chunk-ECOC6TV4.js";
22
+ } from "./chunk-NXITLUZT.js";
23
+ import "./chunk-FHAEFLVV.js";
24
24
  import "./chunk-QGM4M3NI.js";
25
25
  export {
26
26
  CONNECTION_TIMEOUT_MS,
package/dist/index.js CHANGED
@@ -1,17 +1,17 @@
1
1
  import {
2
2
  deploy
3
- } from "./chunk-IPTNRVHY.js";
3
+ } from "./chunk-FBV763OM.js";
4
4
  import {
5
5
  DotNS
6
- } from "./chunk-TITCVJJF.js";
6
+ } from "./chunk-NXITLUZT.js";
7
7
  import {
8
8
  bootstrapPool,
9
9
  derivePoolAccounts,
10
10
  ensureAuthorized,
11
11
  fetchPoolAuthorizations,
12
12
  selectAccount
13
- } from "./chunk-AIHW2WLO.js";
14
- import "./chunk-ECOC6TV4.js";
13
+ } from "./chunk-LGPTJYA3.js";
14
+ import "./chunk-FHAEFLVV.js";
15
15
  import "./chunk-QGM4M3NI.js";
16
16
  export {
17
17
  DotNS,
package/dist/pool.d.ts CHANGED
@@ -14,7 +14,7 @@ interface PoolAuthorization extends PoolAccount {
14
14
  declare function derivePoolAccounts(poolSize?: number, mnemonic?: string): PoolAccount[];
15
15
  declare function selectAccount(authorizations: PoolAuthorization[]): PoolAuthorization | null;
16
16
  declare function fetchPoolAuthorizations(api: any, accounts: PoolAccount[]): Promise<PoolAuthorization[]>;
17
- declare function ensureAuthorized(api: any, poolAccount: PoolAccount, bulletinRpc: string): Promise<void>;
17
+ declare function ensureAuthorized(api: any, address: string, bulletinRpc: string, label?: string): Promise<void>;
18
18
  declare function bootstrapPool(bulletinRpc: string, poolSize?: number, mnemonic?: string): Promise<void>;
19
19
 
20
20
  export { type PoolAccount, type PoolAuthorization, bootstrapPool, derivePoolAccounts, ensureAuthorized, fetchPoolAuthorizations, selectAccount };
package/dist/pool.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  ensureAuthorized,
5
5
  fetchPoolAuthorizations,
6
6
  selectAccount
7
- } from "./chunk-AIHW2WLO.js";
7
+ } from "./chunk-LGPTJYA3.js";
8
8
  import "./chunk-QGM4M3NI.js";
9
9
  export {
10
10
  bootstrapPool,
package/dist/telemetry.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  setDeployAttribute,
9
9
  withDeploySpan,
10
10
  withSpan
11
- } from "./chunk-ECOC6TV4.js";
11
+ } from "./chunk-FHAEFLVV.js";
12
12
  import "./chunk-QGM4M3NI.js";
13
13
  export {
14
14
  VERSION,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulletin-deploy",
3
- "version": "0.6.3",
3
+ "version": "0.6.5-rc.0",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",