bulletin-deploy 0.7.4 → 0.7.6

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.
@@ -12,6 +12,7 @@ var TOPUP_TRANSACTIONS = 1e3;
12
12
  var TOPUP_BYTES = 100000000n;
13
13
  var TOPUP_THRESHOLD_TXS = 50n;
14
14
  var TOPUP_THRESHOLD_BYTES = 50000000n;
15
+ var WS_HEARTBEAT_TIMEOUT_MS = 3e5;
15
16
  function derivePoolAccounts(poolSize = 10, mnemonic = DEV_PHRASE) {
16
17
  const entropy = mnemonicToEntropy(mnemonic);
17
18
  const miniSecret = entropyToMiniSecret(entropy);
@@ -126,23 +127,13 @@ function aliceKeyring() {
126
127
  const signer = getPolkadotSigner(alice.publicKey, "Sr25519", (data) => alice.sign(data));
127
128
  return { alice, signer };
128
129
  }
129
- async function withAliceApi(bulletinRpc, fn) {
130
- const aliceClient = createClient(withPolkadotSdkCompat(getWsProvider(bulletinRpc)));
131
- const aliceApi = aliceClient.getUnsafeApi();
132
- try {
133
- const { signer } = aliceKeyring();
134
- return await fn(aliceApi, signer);
135
- } finally {
136
- aliceClient.destroy();
137
- }
138
- }
139
130
  var U32_MAX = 0xFFFFFFFFn;
140
131
  function clampU32(n, name) {
141
132
  if (n < 0n) throw new Error(`${name} must be non-negative`);
142
133
  if (n > U32_MAX) throw new Error(`${name} (${n}) exceeds u32 max \u2014 split the deploy into smaller batches`);
143
134
  return Number(n);
144
135
  }
145
- async function ensureAuthorized(api, address, bulletinRpc, label, minimum = { txs: TOPUP_THRESHOLD_TXS, bytes: TOPUP_THRESHOLD_BYTES }) {
136
+ async function ensureAuthorized(api, address, label, minimum = { txs: TOPUP_THRESHOLD_TXS, bytes: TOPUP_THRESHOLD_BYTES }) {
146
137
  const auth = await api.query.TransactionStorage.Authorizations.getValue(
147
138
  Enum("Account", address)
148
139
  );
@@ -152,54 +143,55 @@ async function ensureAuthorized(api, address, bulletinRpc, label, minimum = { tx
152
143
  return;
153
144
  }
154
145
  console.log(` Auto-authorizing ${label ?? "account"} (${address.slice(0, 8)}...)...`);
155
- await withAliceApi(bulletinRpc, async (aliceApi, signer) => {
156
- await submitAliceTxWithRetry(
157
- () => aliceApi.tx.TransactionStorage.authorize_account({
158
- who: address,
159
- transactions: TOPUP_TRANSACTIONS,
160
- bytes: TOPUP_BYTES
161
- }),
162
- signer,
163
- `authorize_account(${label ?? "account"})`
164
- );
165
- console.log(` Authorized: ${TOPUP_TRANSACTIONS} txs, ${TOPUP_BYTES / 1000000n}MB`);
166
- });
146
+ const { signer } = aliceKeyring();
147
+ await submitAliceTxWithRetry(
148
+ () => api.tx.TransactionStorage.authorize_account({
149
+ who: address,
150
+ transactions: TOPUP_TRANSACTIONS,
151
+ bytes: TOPUP_BYTES
152
+ }),
153
+ signer,
154
+ `authorize_account(${label ?? "account"})`
155
+ );
156
+ console.log(` Authorized: ${TOPUP_TRANSACTIONS} txs, ${TOPUP_BYTES / 1000000n}MB`);
167
157
  }
168
- async function topUpBy(address, bulletinRpc, needs, label) {
169
- await withAliceApi(bulletinRpc, async (aliceApi, signer) => {
170
- const currentAuth = await aliceApi.query.TransactionStorage.Authorizations.getValue(
171
- Enum("Account", address)
172
- );
173
- const current = {
174
- transactions: currentAuth ? BigInt(currentAuth.extent.transactions) : 0n,
175
- bytes: currentAuth ? currentAuth.extent.bytes : 0n
176
- };
177
- const fmtMB = (b) => (Number(b) / 1e6).toFixed(1);
178
- const target = computeTopUpTarget(current, needs);
179
- if (!target) {
180
- console.log(` Pre-auth skipped for ${label ?? "account"} (${address.slice(0, 8)}...): current ${current.transactions} txs / ${fmtMB(current.bytes)}MB covers needs + floor.`);
181
- return;
182
- }
183
- const transactions = clampU32(target.transactions, "topUpBy.txs");
184
- console.log(` Pre-authorizing ${label ?? "account"} (${address.slice(0, 8)}...): current ${current.transactions} txs / ${fmtMB(current.bytes)}MB \u2192 target ${target.transactions} txs / ${fmtMB(target.bytes)}MB...`);
185
- await submitAliceTxWithRetry(
186
- () => aliceApi.tx.TransactionStorage.authorize_account({
187
- who: address,
188
- transactions,
189
- bytes: target.bytes
190
- }),
191
- signer,
192
- `topUpBy(${label ?? "account"})`
193
- );
194
- console.log(` Pre-authorized: target ${transactions} txs / ${fmtMB(target.bytes)}MB`);
195
- });
158
+ async function topUpBy(api, address, needs, label) {
159
+ const currentAuth = await api.query.TransactionStorage.Authorizations.getValue(
160
+ Enum("Account", address)
161
+ );
162
+ const current = {
163
+ transactions: currentAuth ? BigInt(currentAuth.extent.transactions) : 0n,
164
+ bytes: currentAuth ? currentAuth.extent.bytes : 0n
165
+ };
166
+ const fmtMB = (b) => (Number(b) / 1e6).toFixed(1);
167
+ const target = computeTopUpTarget(current, needs);
168
+ if (!target) {
169
+ console.log(` Pre-auth skipped for ${label ?? "account"} (${address.slice(0, 8)}...): current ${current.transactions} txs / ${fmtMB(current.bytes)}MB covers needs + floor.`);
170
+ return;
171
+ }
172
+ const transactions = clampU32(target.transactions, "topUpBy.txs");
173
+ const { signer } = aliceKeyring();
174
+ console.log(` Pre-authorizing ${label ?? "account"} (${address.slice(0, 8)}...): current ${current.transactions} txs / ${fmtMB(current.bytes)}MB \u2192 target ${target.transactions} txs / ${fmtMB(target.bytes)}MB...`);
175
+ await submitAliceTxWithRetry(
176
+ () => api.tx.TransactionStorage.authorize_account({
177
+ who: address,
178
+ transactions,
179
+ bytes: target.bytes
180
+ }),
181
+ signer,
182
+ `topUpBy(${label ?? "account"})`
183
+ );
184
+ console.log(` Pre-authorized: target ${transactions} txs / ${fmtMB(target.bytes)}MB`);
196
185
  }
197
186
  async function bootstrapPool(bulletinRpc, poolSize = 10, mnemonic) {
198
187
  console.log(`Bootstrapping ${poolSize} pool accounts on ${bulletinRpc}...
199
188
  `);
200
189
  await cryptoWaitReady();
201
190
  const accounts = derivePoolAccounts(poolSize, mnemonic);
202
- const client = createClient(withPolkadotSdkCompat(getWsProvider(bulletinRpc)));
191
+ const client = createClient(withPolkadotSdkCompat(getWsProvider(
192
+ bulletinRpc,
193
+ { heartbeatTimeout: WS_HEARTBEAT_TIMEOUT_MS }
194
+ )));
203
195
  const api = client.getUnsafeApi();
204
196
  const keyring = new Keyring({ type: "sr25519" });
205
197
  const alice = keyring.addFromUri("//Alice");
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  package_default,
3
3
  writeRunState
4
- } from "./chunk-UJP2PZGU.js";
4
+ } from "./chunk-HHY32NGJ.js";
5
5
 
6
6
  // src/memory-report.ts
7
7
  import * as fs2 from "fs";
@@ -2,11 +2,11 @@ import {
2
2
  classifyErrorArea,
3
3
  isInteractive,
4
4
  promptYesNo
5
- } from "./chunk-CQ753LDA.js";
5
+ } from "./chunk-EECNTEAE.js";
6
6
  import {
7
7
  VERSION,
8
8
  getCurrentSentryTraceId
9
- } from "./chunk-DHQ3JGF4.js";
9
+ } from "./chunk-Y6CTPQS2.js";
10
10
 
11
11
  // src/bug-report.ts
12
12
  import { execSync, execFileSync } from "child_process";
@@ -74,7 +74,6 @@ function scrubSecrets(text) {
74
74
  }
75
75
  function buildCliFlagsSummary(flags) {
76
76
  const parts = [];
77
- if (flags.bootstrap) parts.push("--bootstrap");
78
77
  if (flags.jsMerkle) parts.push("--js-merkle");
79
78
  if (flags.ghPagesMirror) parts.push("--gh-pages-mirror");
80
79
  if (flags.poolSize != null) parts.push(`--pool-size ${String(flags.poolSize)}`);
package/dist/deploy.js CHANGED
@@ -25,15 +25,15 @@ import {
25
25
  storeChunkedContent,
26
26
  storeDirectory,
27
27
  storeFile
28
- } from "./chunk-SVKCVNXD.js";
29
- import "./chunk-YYULF2JX.js";
30
- import "./chunk-CQ753LDA.js";
31
- import "./chunk-YREZFNCC.js";
28
+ } from "./chunk-A2J6R5PD.js";
29
+ import "./chunk-YX62STIA.js";
30
+ import "./chunk-EECNTEAE.js";
31
+ import "./chunk-G6CVI6U2.js";
32
32
  import "./chunk-2Q2WSKFD.js";
33
- import "./chunk-DHQ3JGF4.js";
34
- import "./chunk-UJP2PZGU.js";
33
+ import "./chunk-Y6CTPQS2.js";
34
+ import "./chunk-HHY32NGJ.js";
35
35
  import "./chunk-B7GUYYAN.js";
36
- import "./chunk-JHNW2EKY.js";
36
+ import "./chunk-WIBZPZSY.js";
37
37
  import "./chunk-QGM4M3NI.js";
38
38
  export {
39
39
  CHUNK_MORTALITY_PERIOD,
package/dist/dotns.d.ts CHANGED
@@ -88,6 +88,7 @@ declare function stripTrailingDigits(label: string): string;
88
88
  declare function sanitizeDomainLabel(label: string): string;
89
89
  declare function validateDomainLabel(label: string): string;
90
90
  declare function isCommitmentMature(chainNowSeconds: number, commitTimestampSeconds: number, minimumAgeSeconds: number): boolean;
91
+ declare function isExplicitCommitmentBuffer(envValue: string | undefined): boolean;
91
92
  declare function classifyDotnsLabel(label: string): {
92
93
  status: number;
93
94
  message: string;
@@ -155,4 +156,4 @@ declare class DotNS {
155
156
  }
156
157
  declare const dotns: DotNS;
157
158
 
158
- export { CONNECTION_TIMEOUT_MS, CONTRACTS, DECIMALS, DEFAULT_MNEMONIC, DOTNS_TX_MAX_ATTEMPTS, DOT_NODE, DotNS, type DotNSConnectOptions, type DotnsPreflightResult, NATIVE_TO_ETH_RATIO, OPERATION_TIMEOUT_MS, type OwnershipResult, type ParsedDomainName, type PriceValidationResult, ProofOfPersonhoodStatus, RPC_ENDPOINTS, TX_CHAIN_TIME_BUDGET_MS, TX_TIMEOUT_MS, TX_WALL_CLOCK_CEILING_MS, WS_HEARTBEAT_TIMEOUT_MS, canRegister, classifyDotnsLabel, classifyTxRetryDecision, computeDomainTokenId, convertWeiToNative, countTrailingDigits, dotns, fetchNonce, isCommitmentMature, parseDomainName, parseProofOfPersonhoodStatus, popStatusName, runDotnsCli, sanitizeDomainLabel, simulateUserStatus, stripTrailingDigits, validateDomainLabel, verifyNonceAdvanced };
159
+ export { CONNECTION_TIMEOUT_MS, CONTRACTS, DECIMALS, DEFAULT_MNEMONIC, DOTNS_TX_MAX_ATTEMPTS, DOT_NODE, DotNS, type DotNSConnectOptions, type DotnsPreflightResult, NATIVE_TO_ETH_RATIO, OPERATION_TIMEOUT_MS, type OwnershipResult, type ParsedDomainName, type PriceValidationResult, ProofOfPersonhoodStatus, RPC_ENDPOINTS, TX_CHAIN_TIME_BUDGET_MS, TX_TIMEOUT_MS, TX_WALL_CLOCK_CEILING_MS, WS_HEARTBEAT_TIMEOUT_MS, canRegister, classifyDotnsLabel, classifyTxRetryDecision, computeDomainTokenId, convertWeiToNative, countTrailingDigits, dotns, fetchNonce, isCommitmentMature, isExplicitCommitmentBuffer, parseDomainName, parseProofOfPersonhoodStatus, popStatusName, runDotnsCli, sanitizeDomainLabel, simulateUserStatus, stripTrailingDigits, validateDomainLabel, verifyNonceAdvanced };
package/dist/dotns.js CHANGED
@@ -23,6 +23,7 @@ import {
23
23
  dotns,
24
24
  fetchNonce,
25
25
  isCommitmentMature,
26
+ isExplicitCommitmentBuffer,
26
27
  parseDomainName,
27
28
  parseProofOfPersonhoodStatus,
28
29
  popStatusName,
@@ -32,10 +33,10 @@ import {
32
33
  stripTrailingDigits,
33
34
  validateDomainLabel,
34
35
  verifyNonceAdvanced
35
- } from "./chunk-YREZFNCC.js";
36
- import "./chunk-DHQ3JGF4.js";
37
- import "./chunk-UJP2PZGU.js";
38
- import "./chunk-JHNW2EKY.js";
36
+ } from "./chunk-G6CVI6U2.js";
37
+ import "./chunk-Y6CTPQS2.js";
38
+ import "./chunk-HHY32NGJ.js";
39
+ import "./chunk-WIBZPZSY.js";
39
40
  import "./chunk-QGM4M3NI.js";
40
41
  export {
41
42
  CONNECTION_TIMEOUT_MS,
@@ -62,6 +63,7 @@ export {
62
63
  dotns,
63
64
  fetchNonce,
64
65
  isCommitmentMature,
66
+ isExplicitCommitmentBuffer,
65
67
  parseDomainName,
66
68
  parseProofOfPersonhoodStatus,
67
69
  popStatusName,
package/dist/index.js CHANGED
@@ -1,14 +1,14 @@
1
1
  import {
2
2
  deploy
3
- } from "./chunk-SVKCVNXD.js";
4
- import "./chunk-YYULF2JX.js";
5
- import "./chunk-CQ753LDA.js";
3
+ } from "./chunk-A2J6R5PD.js";
4
+ import "./chunk-YX62STIA.js";
5
+ import "./chunk-EECNTEAE.js";
6
6
  import {
7
7
  DotNS,
8
8
  parseDomainName
9
- } from "./chunk-YREZFNCC.js";
9
+ } from "./chunk-G6CVI6U2.js";
10
10
  import "./chunk-2Q2WSKFD.js";
11
- import "./chunk-DHQ3JGF4.js";
11
+ import "./chunk-Y6CTPQS2.js";
12
12
  import {
13
13
  VERSION,
14
14
  loadRunState,
@@ -18,7 +18,7 @@ import {
18
18
  shouldSkipStaleWarning,
19
19
  stateFilePath,
20
20
  writeRunState
21
- } from "./chunk-UJP2PZGU.js";
21
+ } from "./chunk-HHY32NGJ.js";
22
22
  import {
23
23
  merkleizeJS
24
24
  } from "./chunk-B7GUYYAN.js";
@@ -28,7 +28,7 @@ import {
28
28
  ensureAuthorized,
29
29
  fetchPoolAuthorizations,
30
30
  selectAccount
31
- } from "./chunk-JHNW2EKY.js";
31
+ } from "./chunk-WIBZPZSY.js";
32
32
  import "./chunk-QGM4M3NI.js";
33
33
  export {
34
34
  DotNS,
@@ -5,8 +5,8 @@ import {
5
5
  maybeWriteMemoryReport,
6
6
  safeHeap,
7
7
  sampleFromBytes
8
- } from "./chunk-DHQ3JGF4.js";
9
- import "./chunk-UJP2PZGU.js";
8
+ } from "./chunk-Y6CTPQS2.js";
9
+ import "./chunk-HHY32NGJ.js";
10
10
  import "./chunk-QGM4M3NI.js";
11
11
  export {
12
12
  DEFAULT_THRESHOLD_MB,
package/dist/pool.d.ts CHANGED
@@ -30,8 +30,8 @@ declare function classifyAliceTxError(err: unknown): TxRetryDecision;
30
30
  declare function isTestnetSpecName(specName: string | undefined | null): boolean;
31
31
  declare function detectTestnet(api: any): Promise<boolean>;
32
32
  declare function _resetTestnetCacheForTests(): void;
33
- declare function ensureAuthorized(api: any, address: string, bulletinRpc: string, label?: string, minimum?: AuthorizationNeeds): Promise<void>;
34
- declare function topUpBy(address: string, bulletinRpc: string, needs: AuthorizationNeeds, label?: string): Promise<void>;
33
+ declare function ensureAuthorized(api: any, address: string, label?: string, minimum?: AuthorizationNeeds): Promise<void>;
34
+ declare function topUpBy(api: any, address: string, needs: AuthorizationNeeds, label?: string): Promise<void>;
35
35
  declare function bootstrapPool(bulletinRpc: string, poolSize?: number, mnemonic?: string): Promise<void>;
36
36
 
37
37
  export { type AuthorizationNeeds, type PoolAccount, type PoolAuthorization, type TxRetryDecision, _resetTestnetCacheForTests, bootstrapPool, classifyAliceTxError, computeTopUpTarget, derivePoolAccounts, detectTestnet, ensureAuthorized, fetchPoolAuthorizations, isTestnetSpecName, selectAccount, topUpBy };
package/dist/pool.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  isTestnetSpecName,
11
11
  selectAccount,
12
12
  topUpBy
13
- } from "./chunk-JHNW2EKY.js";
13
+ } from "./chunk-WIBZPZSY.js";
14
14
  import "./chunk-QGM4M3NI.js";
15
15
  export {
16
16
  _resetTestnetCacheForTests,
package/dist/run-state.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  shouldSkipStaleWarning,
8
8
  stateFilePath,
9
9
  writeRunState
10
- } from "./chunk-UJP2PZGU.js";
10
+ } from "./chunk-HHY32NGJ.js";
11
11
  import "./chunk-QGM4M3NI.js";
12
12
  export {
13
13
  VERSION,
package/dist/telemetry.js CHANGED
@@ -24,8 +24,8 @@ import {
24
24
  truncateAddress,
25
25
  withDeploySpan,
26
26
  withSpan
27
- } from "./chunk-DHQ3JGF4.js";
28
- import "./chunk-UJP2PZGU.js";
27
+ } from "./chunk-Y6CTPQS2.js";
28
+ import "./chunk-HHY32NGJ.js";
29
29
  import "./chunk-QGM4M3NI.js";
30
30
  export {
31
31
  VERSION,
@@ -8,9 +8,9 @@ import {
8
8
  isPreReleaseVersion,
9
9
  preReleaseWarning,
10
10
  promptYesNo
11
- } from "./chunk-CQ753LDA.js";
12
- import "./chunk-DHQ3JGF4.js";
13
- import "./chunk-UJP2PZGU.js";
11
+ } from "./chunk-EECNTEAE.js";
12
+ import "./chunk-Y6CTPQS2.js";
13
+ import "./chunk-HHY32NGJ.js";
14
14
  import "./chunk-QGM4M3NI.js";
15
15
  export {
16
16
  assessVersion,
@@ -0,0 +1,49 @@
1
+ # bulletin-bootstrap
2
+
3
+ `bulletin-bootstrap` is the operator CLI for initializing Bulletin pool accounts. It is separate from `bulletin-deploy` on purpose: deploys are the normal user path, bootstrap is an admin/setup operation.
4
+
5
+ ## Usage
6
+
7
+ ```bash
8
+ bulletin-bootstrap
9
+ ```
10
+
11
+ Options:
12
+
13
+ | Flag | What it does |
14
+ |---|---|
15
+ | `--rpc wss://...` | Override the Bulletin RPC endpoint. Also readable from `BULLETIN_RPC`. |
16
+ | `--pool-size N` | Number of derived pool accounts to initialize. Default: `10`. |
17
+ | `--mnemonic "..."` | Root mnemonic used to derive the pool accounts. Also readable from `BULLETIN_POOL_MNEMONIC`, then `MNEMONIC`. |
18
+ | `--version` | Print the CLI version. |
19
+ | `--help` | Show help. |
20
+
21
+ ## What it does
22
+
23
+ The command derives the pool account set and initializes the on-chain authorization state needed for Bulletin uploads.
24
+
25
+ Use it when:
26
+
27
+ - you are bringing up a fresh pool on a testnet
28
+ - the shared uploader pool has not been authorized yet
29
+ - you want to pre-initialize a non-default pool mnemonic
30
+
31
+ Do not use it as part of routine deploys. Normal deploys go through `bulletin-deploy`.
32
+
33
+ ## Examples
34
+
35
+ ```bash
36
+ # Default testnet pool against the default RPC
37
+ bulletin-bootstrap
38
+
39
+ # Different RPC and larger pool
40
+ bulletin-bootstrap --rpc wss://custom-bulletin.example.com --pool-size 20
41
+
42
+ # Explicit pool mnemonic
43
+ bulletin-bootstrap --mnemonic "..."
44
+ ```
45
+
46
+ ## Related Docs
47
+
48
+ - [Main README](../README.md)
49
+ - [E2E test setup](./e2e-bootstrap.md)
@@ -0,0 +1,64 @@
1
+ # E2E test setup
2
+
3
+ The E2E suite (`test/e2e.test.js`, driven by `.github/workflows/e2e.yml`) deploys real content to Paseo Bulletin testnet via `bulletin-deploy` and verifies the on-chain round-trip. It consumes the **shared default pool** (derived from `DEV_PHRASE` — the same pool real users hit in production) for Bulletin chunk upload, so no pool bootstrapping is required.
4
+
5
+ Three one-time setup items are needed before the workflow can pass. Do them once per testnet lifetime (redo if testnet is wiped).
6
+
7
+ ## Prerequisites
8
+
9
+ - `bulletin-deploy` built locally (`npm run build`).
10
+ - `@parity/dotns-cli` on `$PATH` (for the Bob registration in item 3). Install with `npm i -g @parity/dotns-cli`.
11
+ - Network access to Paseo Bulletin RPC (`wss://paseo-bulletin-rpc.polkadot.io`) and Asset Hub Paseo (DotNS).
12
+ - Alice's dev mnemonic: `bottom drive obey lake curtain smoke basket hold race lonely fit walk`.
13
+
14
+ ## Setup
15
+
16
+ ### 1. Grant Alice PoP Full
17
+
18
+ Both happy-path scenarios (S1, S2) deploy as Alice via DotNS. Registering a new un-reserved base name requires PoP Full:
19
+
20
+ ```bash
21
+ node tools/check-pop-status.mjs --grant full
22
+ ```
23
+
24
+ Idempotent. Re-run if the check shows Alice below Full.
25
+
26
+ ### 2. Fund and map Bob on Asset Hub Paseo
27
+
28
+ Bob (`//Bob` from the dev phrase) is the owner of `e2eowned.dot` (see item 3). He needs:
29
+
30
+ - **Balance** on Asset Hub Paseo for his on-chain fees. Request ~1 PAS from the Paseo faucet at [https://faucet.polkadot.io/](https://faucet.polkadot.io/) sending to Bob's SS58 address `5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty`.
31
+ - **Revive mapping** so he can sign EVM transactions. Running `check-pop-status` against Bob's key URI submits `map_account` automatically on connect if the mapping is absent:
32
+
33
+ ```bash
34
+ node tools/check-pop-status.mjs "bottom drive obey lake curtain smoke basket hold race lonely fit walk//Bob"
35
+ ```
36
+
37
+ Expected output: Bob's SS58, his H160 (`0x41dccbd49b26c50d34355ed86ff0fa9e489d1e01`), and PoP status (`NoStatus (0)` initially). Idempotent.
38
+
39
+ ### 3. Register `e2eowned.dot` directly as Bob
40
+
41
+ The S3 negative scenario asserts that `bulletin-deploy` refuses to deploy to a domain owned by a different account (exit 78 with transfer guidance). Have Bob register the label via `dotns-cli` — no Alice intermediary, no Bulletin content needed (S3 never reads content):
42
+
43
+ ```bash
44
+ dotns register domain -n e2eowned -s full --cb 30 \
45
+ -k "bottom drive obey lake curtain smoke basket hold race lonely fit walk//Bob"
46
+ ```
47
+
48
+ Expected: exit 0, stdout ends with "Operation Complete" and `Domain: e2eowned.dot`. After this, `e2eowned.dot` is owned by Bob's H160 `0x41dccbd49b26c50d34355ed86ff0fa9e489d1e01`. PoP Full is auto-granted on testnet as part of the registration (an 8-char base name requires Full per `classifyDotnsLabel`). The `--cb 30` sets a 30s commitment buffer — the default 6s sometimes misses the 12s `minCommitmentAge` on Asset Hub Paseo.
49
+
50
+ If S3 ever fails because `e2eowned.dot` was transferred back to Alice by mistake, re-run this step to restore Bob's ownership.
51
+
52
+ ## No pre-registration needed for `e2epool.dot` / `e2edirect.dot`
53
+
54
+ The stable happy-path labels auto-register to Alice on first deploy (pool mode and direct mode both resolve to Alice at the DotNS layer — `--mnemonic` only overrides the DotNS signer, not the Bulletin signer; Bulletin always uses the pool). Subsequent runs exercise the update path (new contenthash under existing ownership).
55
+
56
+ ## Verifying locally
57
+
58
+ ```bash
59
+ E2E=1 E2E_SIGNER=pool E2E_MERKLE=js E2E_SCENARIO=s1 \
60
+ BULLETIN_RPC=wss://paseo-bulletin-rpc.polkadot.io \
61
+ npm run test:e2e
62
+ ```
63
+
64
+ Vary `E2E_SCENARIO` (`s1`, `s2`, `s3`), `E2E_SIGNER` (`pool`, `direct`), and `E2E_MERKLE` (`js`, `kubo`) to cover the full CI matrix.
@@ -0,0 +1,59 @@
1
+ # Telemetry
2
+
3
+ Telemetry is **off by default for external users**. It is enabled automatically for known internal Parity contexts and can also be controlled explicitly.
4
+
5
+ ## Opt in / opt out
6
+
7
+ - `BULLETIN_DEPLOY_TELEMETRY=1`: explicit opt-in
8
+ - `BULLETIN_DEPLOY_TELEMETRY=0`: force off
9
+
10
+ Internal detection signals are OR'd together:
11
+
12
+ 1. `GITHUB_REPOSITORY` matches a known internal org
13
+ 2. `RUNNER_NAME` starts with `parity-`
14
+ 3. `git remote get-url origin` points at a known internal org
15
+
16
+ ## What is tracked
17
+
18
+ - deploy duration and success/failure
19
+ - storage phase timing
20
+ - DotNS phase timing
21
+ - pool account selection
22
+ - source metadata such as repo, branch, and CI vs local
23
+ - tool version
24
+
25
+ ## Ambient Sentry mode
26
+
27
+ If another app embeds `bulletin-deploy` and already owns Sentry initialization, set these before importing or invoking the library:
28
+
29
+ ```sh
30
+ BULLETIN_DEPLOY_USE_AMBIENT_SENTRY=1
31
+ BULLETIN_DEPLOY_HOST_APP=<your-app-name>
32
+ ```
33
+
34
+ That makes `bulletin-deploy` reuse the existing Sentry client instead of calling its own `Sentry.init()`.
35
+
36
+ Requirements:
37
+
38
+ - the host app must initialize Sentry first
39
+ - Sentry SDK compatibility still matters
40
+ - quotas and issue grouping remain owned by the host project
41
+
42
+ ## Tagging test traffic
43
+
44
+ Use `--tag` or `DEPLOY_TAG` to separate test and benchmark traffic from real deploys.
45
+
46
+ Examples:
47
+
48
+ ```bash
49
+ bulletin-deploy --tag e2e-ci-pr ./build my-app.dot
50
+ DEPLOY_TAG=load-test bulletin-deploy ./build my-app.dot
51
+ ```
52
+
53
+ Common tags in this repo:
54
+
55
+ - `e2e-ci-pr`
56
+ - `e2e-ci-nightly`
57
+ - `e2e-local-smoke`
58
+ - `e2e-local-pr`
59
+ - `e2e-local-nightly`
@@ -0,0 +1,44 @@
1
+ # Testing
2
+
3
+ The repo has three practical test layers: offline unit tests, live-testnet E2E coverage, and GitHub Actions matrices that exercise the shipped reusable workflow.
4
+
5
+ ## Offline tests
6
+
7
+ ```bash
8
+ npm test
9
+ ```
10
+
11
+ This runs the local Node test suite without network access.
12
+
13
+ ## Live-testnet E2E
14
+
15
+ The E2E suite deploys real content to Paseo Bulletin and verifies the on-chain round-trip.
16
+
17
+ Local launchers:
18
+
19
+ ```bash
20
+ npm run test:e2e:smoke
21
+ npm run test:e2e:pr
22
+ npm run test:e2e:nightly
23
+ ```
24
+
25
+ Quiet mode:
26
+
27
+ ```bash
28
+ E2E_QUIET=1 npm run test:e2e:smoke
29
+ E2E_QUIET=1 npm run test:e2e:pr
30
+ E2E_QUIET=1 npm run test:e2e:nightly
31
+ ```
32
+
33
+ Each scenario writes a JUnit XML report under `e2e-reports/`.
34
+
35
+ For one-time chain setup, see [E2E test setup](./e2e-bootstrap.md).
36
+
37
+ ## CI matrices
38
+
39
+ `.github/workflows/e2e.yml` calls the shipped reusable `.github/workflows/deploy.yml` so the E2E jobs exercise the same path consumers use.
40
+
41
+ - per-PR: stable happy-path coverage plus negative ownership coverage
42
+ - nightly: broader signer, merkleization, and mirror-path coverage
43
+
44
+ E2E deploys are tagged so telemetry can distinguish them from real-user traffic. See [Telemetry](./telemetry.md).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulletin-deploy",
3
- "version": "0.7.4",
3
+ "version": "0.7.6",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",
@@ -14,7 +14,8 @@
14
14
  "main": "./dist/index.js",
15
15
  "types": "./dist/index.d.ts",
16
16
  "bin": {
17
- "bulletin-deploy": "./bin/bulletin-deploy"
17
+ "bulletin-deploy": "./bin/bulletin-deploy",
18
+ "bulletin-bootstrap": "./bin/bulletin-bootstrap"
18
19
  },
19
20
  "exports": {
20
21
  ".": {
@@ -24,12 +25,13 @@
24
25
  },
25
26
  "files": [
26
27
  "dist",
27
- "bin"
28
+ "bin",
29
+ "docs"
28
30
  ],
29
31
  "scripts": {
30
32
  "build": "tsup src/index.ts src/deploy.ts src/dotns.ts src/pool.ts src/telemetry.ts src/memory-report.ts src/merkle.ts src/gh-pages-mirror.ts src/version-check.ts src/bug-report.ts src/run-state.ts --format esm --dts --clean --target node22",
31
33
  "prepare": "npm run build",
32
- "test": "npm run build && node --test test/test.js test/pool.test.js test/helpers/e2e-helpers.test.js",
34
+ "test": "npm run build && node --test test/test.js test/cli-help.test.js test/helpers/e2e-helpers.test.js",
33
35
  "test:e2e": "npm run build && node --test test/e2e.test.js",
34
36
  "test:e2e:smoke": "bash scripts/e2e-pass.sh smoke",
35
37
  "test:e2e:pr": "bash scripts/e2e-pass.sh pr",