@parity/product-sdk-bulletin 0.1.0 → 0.2.1

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.
package/dist/index.js CHANGED
@@ -1,18 +1,22 @@
1
- // src/client.ts
2
- import { getChainAPI } from "@parity/product-sdk-chain-client";
3
-
4
- // src/authorization.ts
5
- import { createLogger } from "@parity/product-sdk-logger";
6
- import { Enum } from "polkadot-api";
1
+ import { parseCid, CidCodec as CidCodec$1, UnixFsDagBuilder, AsyncBulletinClient } from '@parity/bulletin-sdk';
2
+ export { AsyncBulletinClient, AuthCallBuilder, AuthorizationScope, BulletinError, BulletinPreparer, CID, CallBuilder, ChunkStatus, DEFAULT_CHUNKER_CONFIG, DEFAULT_CLIENT_CONFIG, DEFAULT_STORE_OPTIONS, ErrorCode, MAX_CHUNK_SIZE, MAX_FILE_SIZE, MockBulletinClient, StoreBuilder, TxStatus, WaitFor, calculateCid, cidFromBytes, cidToBytes, convertCid, estimateAuthorization, getContentHash, parseCid, reassembleChunks, resolveClientConfig, validateChunkSize } from '@parity/bulletin-sdk';
3
+ import { getChainAPI, createChainClient } from '@parity/product-sdk-chain-client';
4
+ import { createLogger } from '@parity/product-sdk-logger';
5
+ import { submitAndWatch } from '@parity/product-sdk-tx';
6
+ import { Enum } from 'polkadot-api';
7
+ import { getPreimageManager } from '@parity/product-sdk-host';
8
+ import { CID } from 'multiformats/cid';
9
+ import * as Digest from 'multiformats/hashes/digest';
10
+ import { bulletin } from '@parity/product-sdk-descriptors/bulletin';
7
11
 
8
- // src/errors.ts
9
- var BulletinError = class extends Error {
12
+ // src/client.ts
13
+ var ProductBulletinError = class extends Error {
10
14
  constructor(message, options) {
11
15
  super(message, options);
12
- this.name = "BulletinError";
16
+ this.name = "ProductBulletinError";
13
17
  }
14
18
  };
15
- var BulletinHostUnavailableError = class extends BulletinError {
19
+ var BulletinHostUnavailableError = class extends ProductBulletinError {
16
20
  constructor(operation) {
17
21
  super(
18
22
  `Host preimage API unavailable for ${operation}. Ensure you are running inside a host container (Polkadot Browser / Desktop).`
@@ -20,7 +24,7 @@ var BulletinHostUnavailableError = class extends BulletinError {
20
24
  this.name = "BulletinHostUnavailableError";
21
25
  }
22
26
  };
23
- var BulletinLookupTimeoutError = class extends BulletinError {
27
+ var BulletinLookupTimeoutError = class extends ProductBulletinError {
24
28
  /** The CID that was being looked up. */
25
29
  cid;
26
30
  /** The timeout duration in milliseconds. */
@@ -32,7 +36,7 @@ var BulletinLookupTimeoutError = class extends BulletinError {
32
36
  this.timeoutMs = timeoutMs;
33
37
  }
34
38
  };
35
- var BulletinLookupInterruptedError = class extends BulletinError {
39
+ var BulletinLookupInterruptedError = class extends ProductBulletinError {
36
40
  /** The CID that was being looked up. */
37
41
  cid;
38
42
  constructor(cid) {
@@ -41,40 +45,7 @@ var BulletinLookupInterruptedError = class extends BulletinError {
41
45
  this.cid = cid;
42
46
  }
43
47
  };
44
- var BulletinAuthorizationError = class extends BulletinError {
45
- /** The address that was being checked. */
46
- address;
47
- constructor(address, cause) {
48
- super(`Failed to check authorization for ${address}`, { cause });
49
- this.name = "BulletinAuthorizationError";
50
- this.address = address;
51
- }
52
- };
53
- var BulletinGatewayUnavailableError = class extends BulletinError {
54
- /** The environment that was requested. */
55
- environment;
56
- constructor(environment) {
57
- super(`Bulletin gateway for "${environment}" is not yet available`);
58
- this.name = "BulletinGatewayUnavailableError";
59
- this.environment = environment;
60
- }
61
- };
62
- var BulletinGatewayFetchError = class extends BulletinError {
63
- /** The CID that was being fetched. */
64
- cid;
65
- /** The HTTP status code returned by the gateway. */
66
- status;
67
- /** The HTTP status text returned by the gateway. */
68
- statusText;
69
- constructor(cid, status, statusText) {
70
- super(`Gateway fetch failed for ${cid}: ${status} ${statusText}`);
71
- this.name = "BulletinGatewayFetchError";
72
- this.cid = cid;
73
- this.status = status;
74
- this.statusText = statusText;
75
- }
76
- };
77
- var BulletinCidError = class extends BulletinError {
48
+ var BulletinCidError = class extends ProductBulletinError {
78
49
  /** The invalid CID string, if available. */
79
50
  cid;
80
51
  constructor(message, cid) {
@@ -83,84 +54,15 @@ var BulletinCidError = class extends BulletinError {
83
54
  this.cid = cid;
84
55
  }
85
56
  };
86
- if (void 0) {
87
- const { describe, test, expect } = void 0;
88
- describe("BulletinError hierarchy", () => {
89
- test("BulletinError is instanceof Error", () => {
90
- const err = new BulletinError("test");
91
- expect(err).toBeInstanceOf(Error);
92
- expect(err).toBeInstanceOf(BulletinError);
93
- expect(err.name).toBe("BulletinError");
94
- });
95
- test("BulletinHostUnavailableError", () => {
96
- const err = new BulletinHostUnavailableError("upload");
97
- expect(err).toBeInstanceOf(BulletinError);
98
- expect(err.name).toBe("BulletinHostUnavailableError");
99
- expect(err.message).toContain("upload");
100
- expect(err.message).toContain("Host preimage API unavailable");
101
- });
102
- test("BulletinLookupTimeoutError", () => {
103
- const err = new BulletinLookupTimeoutError("bafyabc123", 3e4);
104
- expect(err).toBeInstanceOf(BulletinError);
105
- expect(err.name).toBe("BulletinLookupTimeoutError");
106
- expect(err.cid).toBe("bafyabc123");
107
- expect(err.timeoutMs).toBe(3e4);
108
- expect(err.message).toContain("30000ms");
109
- expect(err.message).toContain("bafyabc123");
110
- });
111
- test("BulletinLookupInterruptedError", () => {
112
- const err = new BulletinLookupInterruptedError("bafyabc123");
113
- expect(err).toBeInstanceOf(BulletinError);
114
- expect(err.name).toBe("BulletinLookupInterruptedError");
115
- expect(err.cid).toBe("bafyabc123");
116
- expect(err.message).toContain("interrupted");
117
- });
118
- test("BulletinAuthorizationError with cause", () => {
119
- const cause = new Error("RPC timeout");
120
- const err = new BulletinAuthorizationError("5GrwvaEF...", cause);
121
- expect(err).toBeInstanceOf(BulletinError);
122
- expect(err.name).toBe("BulletinAuthorizationError");
123
- expect(err.address).toBe("5GrwvaEF...");
124
- expect(err.cause).toBe(cause);
125
- });
126
- test("BulletinGatewayUnavailableError", () => {
127
- const err = new BulletinGatewayUnavailableError("polkadot");
128
- expect(err).toBeInstanceOf(BulletinError);
129
- expect(err.name).toBe("BulletinGatewayUnavailableError");
130
- expect(err.environment).toBe("polkadot");
131
- expect(err.message).toContain("polkadot");
132
- });
133
- test("BulletinGatewayFetchError", () => {
134
- const err = new BulletinGatewayFetchError("bafyabc", 404, "Not Found");
135
- expect(err).toBeInstanceOf(BulletinError);
136
- expect(err.name).toBe("BulletinGatewayFetchError");
137
- expect(err.cid).toBe("bafyabc");
138
- expect(err.status).toBe(404);
139
- expect(err.statusText).toBe("Not Found");
140
- expect(err.message).toContain("404");
141
- });
142
- test("BulletinCidError", () => {
143
- const err = new BulletinCidError("Expected CIDv1, got CIDv0", "Qmabc");
144
- expect(err).toBeInstanceOf(BulletinError);
145
- expect(err.name).toBe("BulletinCidError");
146
- expect(err.cid).toBe("Qmabc");
147
- });
148
- test("all errors can be caught with BulletinError", () => {
149
- const errors = [
150
- new BulletinHostUnavailableError("query"),
151
- new BulletinLookupTimeoutError("cid", 1e3),
152
- new BulletinLookupInterruptedError("cid"),
153
- new BulletinAuthorizationError("addr"),
154
- new BulletinGatewayUnavailableError("env"),
155
- new BulletinGatewayFetchError("cid", 500, "Error"),
156
- new BulletinCidError("bad cid")
157
- ];
158
- for (const err of errors) {
159
- expect(err).toBeInstanceOf(BulletinError);
160
- }
161
- });
162
- });
163
- }
57
+ var BulletinAuthorizationError = class extends ProductBulletinError {
58
+ /** The address that was being checked. */
59
+ address;
60
+ constructor(address, cause) {
61
+ super(`Failed to check authorization for ${address}`, { cause });
62
+ this.name = "BulletinAuthorizationError";
63
+ this.address = address;
64
+ }
65
+ };
164
66
 
165
67
  // src/authorization.ts
166
68
  var log = createLogger("bulletin");
@@ -186,8 +88,11 @@ async function checkAuthorization(api, address) {
186
88
  }
187
89
  const status = {
188
90
  authorized: true,
189
- remainingTransactions: auth.extent.transactions,
190
- remainingBytes: auth.extent.bytes,
91
+ remainingTransactions: auth.extent.transactions_allowance - auth.extent.transactions,
92
+ // `auth` is `any` (TypedApi<any> upstream — see line 53). TS narrows
93
+ // `any - any` to `number`, so cast each u64 operand to bigint to keep
94
+ // the subtraction in bigint space. Runtime values are bigints from PAPI.
95
+ remainingBytes: auth.extent.bytes_allowance - auth.extent.bytes,
191
96
  expiration: auth.expiration
192
97
  };
193
98
  log.debug("checkAuthorization", {
@@ -198,112 +103,41 @@ async function checkAuthorization(api, address) {
198
103
  });
199
104
  return status;
200
105
  }
201
- if (void 0) {
202
- let createMockApi = function(authResult) {
203
- return {
204
- query: {
205
- TransactionStorage: {
206
- Authorizations: {
207
- getValue: vi.fn().mockResolvedValue(authResult)
208
- }
209
- }
210
- }
211
- };
212
- };
213
- createMockApi2 = createMockApi;
214
- const { describe, test, expect, vi } = void 0;
215
- describe("checkAuthorization", () => {
216
- test("returns not authorized when no authorization exists", async () => {
217
- const api = createMockApi(void 0);
218
- const status = await checkAuthorization(api, "5GrwvaEF...");
219
- expect(status.authorized).toBe(false);
220
- expect(status.remainingTransactions).toBe(0);
221
- expect(status.remainingBytes).toBe(0n);
222
- expect(status.expiration).toBe(0);
223
- });
224
- test("returns authorization with full quota", async () => {
225
- const api = createMockApi({
226
- extent: { transactions: 10, bytes: 1000000n },
227
- expiration: 999
228
- });
229
- const status = await checkAuthorization(api, "5GrwvaEF...");
230
- expect(status.authorized).toBe(true);
231
- expect(status.remainingTransactions).toBe(10);
232
- expect(status.remainingBytes).toBe(1000000n);
233
- expect(status.expiration).toBe(999);
234
- });
235
- test("returns authorization with zero transactions remaining", async () => {
236
- const api = createMockApi({
237
- extent: { transactions: 0, bytes: 1000000n },
238
- expiration: 999
239
- });
240
- const status = await checkAuthorization(api, "5GrwvaEF...");
241
- expect(status.authorized).toBe(true);
242
- expect(status.remainingTransactions).toBe(0);
243
- });
244
- test("returns authorization with zero bytes remaining", async () => {
245
- const api = createMockApi({
246
- extent: { transactions: 5, bytes: 0n },
247
- expiration: 999
248
- });
249
- const status = await checkAuthorization(api, "5GrwvaEF...");
250
- expect(status.authorized).toBe(true);
251
- expect(status.remainingBytes).toBe(0n);
252
- });
253
- test("preserves expiration block number", async () => {
254
- const api = createMockApi({
255
- extent: { transactions: 1, bytes: 500n },
256
- expiration: 12345
257
- });
258
- const status = await checkAuthorization(api, "5GrwvaEF...");
259
- expect(status.expiration).toBe(12345);
260
- });
261
- test("throws BulletinAuthorizationError when query fails", async () => {
262
- const api = {
263
- query: {
264
- TransactionStorage: {
265
- Authorizations: {
266
- getValue: vi.fn().mockRejectedValue(new Error("RPC connection lost"))
267
- }
268
- }
269
- }
270
- };
271
- const err = await checkAuthorization(api, "5GrwvaEF...").catch((e) => e);
272
- expect(err).toBeInstanceOf(BulletinAuthorizationError);
273
- const error = err;
274
- expect(error.address).toBe("5GrwvaEF...");
275
- expect(error.cause).toBeInstanceOf(Error);
276
- expect(error.cause.message).toBe("RPC connection lost");
277
- });
278
- test("passes correct Enum key to the query", async () => {
279
- const getValue = vi.fn().mockResolvedValue(void 0);
280
- const api = {
281
- query: {
282
- TransactionStorage: {
283
- Authorizations: { getValue }
284
- }
285
- }
286
- };
287
- await checkAuthorization(api, "5GrwvaEF...");
288
- expect(getValue).toHaveBeenCalledTimes(1);
289
- const arg = getValue.mock.calls[0][0];
290
- expect(arg.type).toBe("Account");
291
- expect(arg.value).toBe("5GrwvaEF...");
292
- });
106
+ async function authorizeAccount(api, who, transactions, bytes, signer, options = {}) {
107
+ const { viaSudo = false, waitFor, timeoutMs, onStatus } = options;
108
+ const apiTx = api.tx;
109
+ log.info("authorizeAccount: building extrinsic", {
110
+ who,
111
+ transactions,
112
+ bytes: bytes.toString(),
113
+ viaSudo
114
+ });
115
+ if (viaSudo && !apiTx.Sudo?.sudo) {
116
+ throw new ProductBulletinError(
117
+ "viaSudo: true requires the Sudo pallet, which is not available on this network. On production networks (Polkadot, Kusama), authorize_account requires governance or a different mechanism."
118
+ );
119
+ }
120
+ const authorizeTx = apiTx.TransactionStorage.authorize_account({
121
+ who,
122
+ transactions,
123
+ bytes
124
+ });
125
+ const txToSubmit = viaSudo ? apiTx.Sudo.sudo({ call: authorizeTx.decodedCall }) : authorizeTx;
126
+ const result = await submitAndWatch(txToSubmit, signer, {
127
+ waitFor,
128
+ timeoutMs,
129
+ onStatus
293
130
  });
131
+ log.info("authorizeAccount: included in block", {
132
+ who,
133
+ blockHash: result.block.hash
134
+ });
135
+ return { blockHash: result.block.hash };
294
136
  }
295
- var createMockApi2;
296
-
297
- // src/cid.ts
298
- import { blake2b256, bytesToHex, hexToBytes } from "@parity/product-sdk-crypto";
299
- import { createLogger as createLogger2 } from "@parity/product-sdk-logger";
300
- import { CID } from "multiformats/cid";
301
- import * as Digest from "multiformats/hashes/digest";
302
- var log2 = createLogger2("bulletin");
303
137
  var HashAlgorithm = {
304
- /** BLAKE2b-256 — default for product-sdk and the chain SDK. */
138
+ /** BLAKE2b-256 — chain default. */
305
139
  Blake2b256: 45600,
306
- /** SHA2-256 — default for bulletin-deploy. */
140
+ /** SHA2-256. */
307
141
  Sha2_256: 18,
308
142
  /** Keccak-256 — Ethereum compatibility. */
309
143
  Keccak256: 27
@@ -311,7 +145,7 @@ var HashAlgorithm = {
311
145
  var CidCodec = {
312
146
  /** Raw binary — default for single-chunk data. */
313
147
  Raw: 85,
314
- /** DAG-PB — used for multi-chunk manifests / directory structures. */
148
+ /** DAG-PB — used for multi-chunk manifests / IPFS UnixFS. */
315
149
  DagPb: 112,
316
150
  /** DAG-CBOR — alternative DAG encoding. */
317
151
  DagCbor: 113
@@ -319,29 +153,17 @@ var CidCodec = {
319
153
  var SUPPORTED_HASH_CODES = new Set(Object.values(HashAlgorithm));
320
154
  var SUPPORTED_CODEC_CODES = new Set(Object.values(CidCodec));
321
155
  var EXPECTED_HEX_LENGTH = 66;
322
- function computeCid(data) {
323
- const hash = blake2b256(data);
324
- return CID.createV1(CidCodec.Raw, Digest.create(HashAlgorithm.Blake2b256, hash)).toString();
325
- }
326
- function cidToPreimageKey(cid) {
327
- const parsed = CID.parse(cid);
328
- if (parsed.version !== 1) {
329
- throw new BulletinCidError(`Expected CIDv1, got CIDv${parsed.version}`, cid);
330
- }
331
- if (!SUPPORTED_HASH_CODES.has(parsed.multihash.code)) {
332
- throw new BulletinCidError(
333
- `Unsupported hash algorithm 0x${parsed.multihash.code.toString(16)}; expected one of: ${[...SUPPORTED_HASH_CODES].map((c) => `0x${c.toString(16)}`).join(", ")}`,
334
- cid
335
- );
336
- }
337
- return `0x${bytesToHex(parsed.multihash.digest)}`;
338
- }
339
156
  function hashToCid(hexHash, hashCode = HashAlgorithm.Blake2b256, codec = CidCodec.Raw) {
340
157
  if (hexHash.length !== EXPECTED_HEX_LENGTH) {
341
158
  throw new BulletinCidError(
342
159
  `Expected a 0x-prefixed 32-byte hex hash (${EXPECTED_HEX_LENGTH} chars), got ${hexHash.length} chars`
343
160
  );
344
161
  }
162
+ if (!/^0x[0-9a-fA-F]{64}$/.test(hexHash)) {
163
+ throw new BulletinCidError(
164
+ `Invalid hash format: expected 0x-prefixed 32-byte hex string, got: ${hexHash}`
165
+ );
166
+ }
345
167
  if (!SUPPORTED_HASH_CODES.has(hashCode)) {
346
168
  throw new BulletinCidError(
347
169
  `Unsupported hash algorithm 0x${hashCode.toString(16)}; expected one of: ${[...SUPPORTED_HASH_CODES].map((c) => `0x${c.toString(16)}`).join(", ")}`
@@ -352,346 +174,49 @@ function hashToCid(hexHash, hashCode = HashAlgorithm.Blake2b256, codec = CidCode
352
174
  `Unsupported CID codec 0x${codec.toString(16)}; expected one of: ${[...SUPPORTED_CODEC_CODES].map((c) => `0x${c.toString(16)}`).join(", ")}`
353
175
  );
354
176
  }
355
- const digest = hexToBytes(hexHash.slice(2));
356
- const cid = CID.createV1(codec, Digest.create(hashCode, digest)).toString();
357
- log2.debug("hashToCid", { hexHash, hashCode, codec, cid });
358
- return cid;
177
+ const digest = hexToBytes(hexHash);
178
+ return CID.createV1(codec, Digest.create(hashCode, digest)).toString();
359
179
  }
360
- if (void 0) {
361
- const { describe, test, expect } = void 0;
362
- describe("computeCid", () => {
363
- test("produces known CID for known input", () => {
364
- const data = new TextEncoder().encode("hello bulletin");
365
- const cid = computeCid(data);
366
- expect(cid).toBe(computeCid(new TextEncoder().encode("hello bulletin")));
367
- expect(cid).toMatch(/^b[a-z2-7]+$/);
368
- });
369
- test("deterministic \u2014 same input, same output", () => {
370
- const data = new Uint8Array([1, 2, 3, 4, 5]);
371
- expect(computeCid(data)).toBe(computeCid(data));
372
- });
373
- test("different inputs produce different CIDs", () => {
374
- const a = computeCid(new Uint8Array([1]));
375
- const b = computeCid(new Uint8Array([2]));
376
- expect(a).not.toBe(b);
377
- });
378
- test("empty input produces valid CID", () => {
379
- const cid = computeCid(new Uint8Array(0));
380
- expect(cid).toMatch(/^b[a-z2-7]+$/);
381
- });
382
- test("matches reference implementation (manual varint)", () => {
383
- const data = new TextEncoder().encode("test");
384
- const cid = computeCid(data);
385
- expect(cid[0]).toBe("b");
386
- const parsed = CID.parse(cid);
387
- expect(parsed.version).toBe(1);
388
- expect(parsed.code).toBe(CidCodec.Raw);
389
- });
390
- });
391
- describe("cidToPreimageKey", () => {
392
- test("round-trips with computeCid \u2014 returns 0x-prefixed 64-char hex", () => {
393
- const data = new TextEncoder().encode("hello bulletin");
394
- const cid = computeCid(data);
395
- const key = cidToPreimageKey(cid);
396
- expect(key).toMatch(/^0x[0-9a-f]{64}$/);
397
- });
398
- test("deterministic \u2014 same CID always yields same key", () => {
399
- const cid = computeCid(new Uint8Array([1, 2, 3]));
400
- expect(cidToPreimageKey(cid)).toBe(cidToPreimageKey(cid));
401
- });
402
- test("matches raw blake2b-256 hash", () => {
403
- const data = new TextEncoder().encode("test");
404
- const cid = computeCid(data);
405
- const key = cidToPreimageKey(cid);
406
- const hash = blake2b256(data);
407
- const expected = `0x${bytesToHex(hash)}`;
408
- expect(key).toBe(expected);
409
- });
410
- test("accepts CIDv1 with sha2-256", () => {
411
- const hash = new Uint8Array(32).fill(205);
412
- const cidV1 = CID.createV1(CidCodec.Raw, Digest.create(HashAlgorithm.Sha2_256, hash));
413
- const key = cidToPreimageKey(cidV1.toString());
414
- expect(key).toMatch(/^0x[0-9a-f]{64}$/);
415
- expect(key).toBe(`0x${bytesToHex(hash)}`);
416
- });
417
- test("accepts CIDv1 with keccak-256", () => {
418
- const hash = new Uint8Array(32).fill(239);
419
- const cidV1 = CID.createV1(CidCodec.Raw, Digest.create(HashAlgorithm.Keccak256, hash));
420
- const key = cidToPreimageKey(cidV1.toString());
421
- expect(key).toBe(`0x${bytesToHex(hash)}`);
422
- });
423
- test("throws BulletinCidError for CIDv0 input", () => {
424
- const hash = new Uint8Array(32).fill(171);
425
- const cidV0 = CID.create(0, 112, Digest.create(HashAlgorithm.Sha2_256, hash));
426
- expect(() => cidToPreimageKey(cidV0.toString())).toThrow(BulletinCidError);
427
- });
428
- test("throws BulletinCidError for CIDv1 with unsupported hash algorithm", () => {
429
- const unsupportedCode = 153;
430
- const hash = new Uint8Array(32).fill(171);
431
- const cidV1 = CID.createV1(CidCodec.Raw, Digest.create(unsupportedCode, hash));
432
- expect(() => cidToPreimageKey(cidV1.toString())).toThrow(BulletinCidError);
433
- });
434
- });
435
- describe("hashToCid", () => {
436
- test("round-trips with cidToPreimageKey \u2014 hex \u2192 CID \u2192 hex", () => {
437
- const data = new TextEncoder().encode("hello bulletin");
438
- const originalCid = computeCid(data);
439
- const hex = cidToPreimageKey(originalCid);
440
- const reconstructed = hashToCid(hex);
441
- expect(reconstructed).toBe(originalCid);
442
- });
443
- test("full cycle: data \u2192 CID \u2192 hex \u2192 CID", () => {
444
- const data = new Uint8Array([10, 20, 30, 40, 50]);
445
- const cid1 = computeCid(data);
446
- const hex = cidToPreimageKey(cid1);
447
- const cid2 = hashToCid(hex);
448
- expect(cid2).toBe(cid1);
449
- });
450
- test("deterministic \u2014 same hex always yields same CID", () => {
451
- const hex = cidToPreimageKey(computeCid(new Uint8Array([1, 2, 3])));
452
- expect(hashToCid(hex)).toBe(hashToCid(hex));
453
- });
454
- test("produces valid base32-lower CIDv1 (default: blake2b-256, raw)", () => {
455
- const hex = cidToPreimageKey(computeCid(new TextEncoder().encode("test")));
456
- const cid = hashToCid(hex);
457
- expect(cid).toMatch(/^b[a-z2-7]+$/);
458
- const parsed = CID.parse(cid);
459
- expect(parsed.version).toBe(1);
460
- expect(parsed.code).toBe(CidCodec.Raw);
461
- expect(parsed.multihash.code).toBe(HashAlgorithm.Blake2b256);
462
- });
463
- test("sha2-256 produces different CID from blake2b-256 for same hash", () => {
464
- const hex = `0x${"ab".repeat(32)}`;
465
- const blake = hashToCid(hex, HashAlgorithm.Blake2b256);
466
- const sha = hashToCid(hex, HashAlgorithm.Sha2_256);
467
- expect(blake).not.toBe(sha);
468
- expect(CID.parse(blake).version).toBe(1);
469
- expect(CID.parse(sha).version).toBe(1);
470
- });
471
- test("sha2-256 round-trips through cidToPreimageKey", () => {
472
- const hex = `0x${"cd".repeat(32)}`;
473
- const cid = hashToCid(hex, HashAlgorithm.Sha2_256);
474
- const extracted = cidToPreimageKey(cid);
475
- expect(extracted).toBe(hex);
476
- });
477
- test("keccak-256 round-trips through cidToPreimageKey", () => {
478
- const hex = `0x${"ef".repeat(32)}`;
479
- const cid = hashToCid(hex, HashAlgorithm.Keccak256);
480
- const extracted = cidToPreimageKey(cid);
481
- expect(extracted).toBe(hex);
482
- });
483
- test("dag-pb codec produces different CID from raw for same hash", () => {
484
- const hex = `0x${"ab".repeat(32)}`;
485
- const rawCid = hashToCid(hex, HashAlgorithm.Blake2b256, CidCodec.Raw);
486
- const dagPbCid = hashToCid(hex, HashAlgorithm.Blake2b256, CidCodec.DagPb);
487
- expect(rawCid).not.toBe(dagPbCid);
488
- expect(CID.parse(dagPbCid).code).toBe(CidCodec.DagPb);
489
- });
490
- test("dag-cbor codec works", () => {
491
- const hex = `0x${"ab".repeat(32)}`;
492
- const cid = hashToCid(hex, HashAlgorithm.Blake2b256, CidCodec.DagCbor);
493
- expect(CID.parse(cid).code).toBe(CidCodec.DagCbor);
494
- });
495
- test("throws BulletinCidError for hex that is too short", () => {
496
- expect(() => hashToCid("0xabcd")).toThrow(BulletinCidError);
497
- });
498
- test("throws BulletinCidError for hex that is too long", () => {
499
- const tooLong = `0x${"aa".repeat(33)}`;
500
- expect(() => hashToCid(tooLong)).toThrow(BulletinCidError);
501
- });
502
- test("throws for non-hex characters", () => {
503
- const bad = `0x${"zz".repeat(32)}`;
504
- expect(() => hashToCid(bad)).toThrow();
505
- });
506
- test("throws BulletinCidError for unsupported hash algorithm", () => {
507
- const hex = `0x${"ab".repeat(32)}`;
508
- expect(() => hashToCid(hex, 153)).toThrow(BulletinCidError);
509
- });
510
- test("throws BulletinCidError for unsupported codec", () => {
511
- const hex = `0x${"ab".repeat(32)}`;
512
- expect(() => hashToCid(hex, HashAlgorithm.Blake2b256, 153)).toThrow(
513
- BulletinCidError
514
- );
515
- });
516
- });
517
- }
518
-
519
- // src/gateway.ts
520
- var GATEWAYS = {
521
- paseo: "https://paseo-ipfs.polkadot.io/ipfs/"
522
- };
523
- var DEFAULT_FETCH_TIMEOUT_MS = 3e4;
524
- function getGateway(env) {
525
- const gw = GATEWAYS[env];
526
- if (!gw) {
527
- throw new BulletinGatewayUnavailableError(env);
528
- }
529
- return gw;
530
- }
531
- function gatewayUrl(cid, gateway) {
532
- return `${gateway}${cid}`;
533
- }
534
- async function cidExists(cid, gateway, options) {
535
- const timeoutMs = options?.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
536
- const controller = new AbortController();
537
- const timer = setTimeout(() => controller.abort(), timeoutMs);
180
+ function cidToPreimageKey(cid) {
181
+ let parsed;
538
182
  try {
539
- const response = await fetch(gatewayUrl(cid, gateway), {
540
- method: "HEAD",
541
- signal: controller.signal
542
- });
543
- return response.ok;
183
+ parsed = CID.parse(cid);
544
184
  } catch {
545
- return false;
546
- } finally {
547
- clearTimeout(timer);
185
+ throw new BulletinCidError(`Invalid CID: ${cid}`, cid);
548
186
  }
549
- }
550
- async function fetchBytes(cid, gateway, options) {
551
- const timeoutMs = options?.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
552
- const controller = new AbortController();
553
- const timer = setTimeout(() => controller.abort(), timeoutMs);
554
- try {
555
- const response = await fetch(gatewayUrl(cid, gateway), { signal: controller.signal });
556
- if (!response.ok) {
557
- throw new BulletinGatewayFetchError(cid, response.status, response.statusText);
558
- }
559
- return new Uint8Array(await response.arrayBuffer());
560
- } finally {
561
- clearTimeout(timer);
187
+ if (parsed.version !== 1) {
188
+ throw new BulletinCidError(`Expected CIDv1, got CIDv${parsed.version}`, cid);
189
+ }
190
+ if (!SUPPORTED_HASH_CODES.has(parsed.multihash.code)) {
191
+ throw new BulletinCidError(
192
+ `Unsupported hash algorithm 0x${parsed.multihash.code.toString(16)}; expected one of: ${[...SUPPORTED_HASH_CODES].map((c) => `0x${c.toString(16)}`).join(", ")}`,
193
+ cid
194
+ );
562
195
  }
196
+ return `0x${bytesToHex(parsed.multihash.digest)}`;
563
197
  }
564
- async function fetchJson(cid, gateway, options) {
565
- const bytes = await fetchBytes(cid, gateway, options);
566
- return JSON.parse(new TextDecoder().decode(bytes));
198
+ function hexToBytes(hex) {
199
+ const out = new Uint8Array(32);
200
+ for (let i = 0; i < 32; i++) {
201
+ out[i] = Number.parseInt(hex.slice(2 + i * 2, 4 + i * 2), 16);
202
+ }
203
+ return out;
567
204
  }
568
- if (void 0) {
569
- const { describe, test, expect, vi, afterEach } = void 0;
570
- afterEach(() => {
571
- vi.restoreAllMocks();
572
- });
573
- describe("getGateway", () => {
574
- test("returns known URL for paseo", () => {
575
- const gw = getGateway("paseo");
576
- expect(gw).toMatch(/^https:\/\//);
577
- expect(gw).toMatch(/\/ipfs\/$/);
578
- });
579
- test("throws BulletinGatewayUnavailableError for environments without a live gateway", () => {
580
- expect(() => getGateway("polkadot")).toThrow(BulletinGatewayUnavailableError);
581
- expect(() => getGateway("kusama")).toThrow(BulletinGatewayUnavailableError);
582
- });
583
- });
584
- describe("gatewayUrl", () => {
585
- test("concatenates gateway and CID", () => {
586
- expect(gatewayUrl("bafyabc", "https://gw.example/ipfs/")).toBe(
587
- "https://gw.example/ipfs/bafyabc"
588
- );
589
- });
590
- });
591
- describe("cidExists", () => {
592
- test("returns true for 200 response", async () => {
593
- vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ ok: true }));
594
- expect(await cidExists("bafyabc", "https://gw/ipfs/")).toBe(true);
595
- });
596
- test("returns false for 404 response", async () => {
597
- vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ ok: false, status: 404 }));
598
- expect(await cidExists("bafyabc", "https://gw/ipfs/")).toBe(false);
599
- });
600
- test("returns false on network error", async () => {
601
- vi.stubGlobal("fetch", vi.fn().mockRejectedValue(new Error("network")));
602
- expect(await cidExists("bafyabc", "https://gw/ipfs/")).toBe(false);
603
- });
604
- test("returns false on timeout", async () => {
605
- vi.stubGlobal(
606
- "fetch",
607
- vi.fn().mockImplementation(
608
- (_url, init) => new Promise((_resolve, reject) => {
609
- init.signal.addEventListener(
610
- "abort",
611
- () => reject(new DOMException("aborted", "AbortError"))
612
- );
613
- })
614
- )
615
- );
616
- expect(await cidExists("bafyabc", "https://gw/ipfs/", { timeoutMs: 10 })).toBe(false);
617
- });
618
- });
619
- describe("fetchBytes", () => {
620
- test("returns bytes from response", async () => {
621
- const payload = new Uint8Array([1, 2, 3]);
622
- vi.stubGlobal(
623
- "fetch",
624
- vi.fn().mockResolvedValue({
625
- ok: true,
626
- arrayBuffer: () => Promise.resolve(payload.buffer)
627
- })
628
- );
629
- const result = await fetchBytes("bafyabc", "https://gw/ipfs/");
630
- expect(result).toEqual(payload);
631
- });
632
- test("throws BulletinGatewayFetchError on non-ok response", async () => {
633
- vi.stubGlobal(
634
- "fetch",
635
- vi.fn().mockResolvedValue({
636
- ok: false,
637
- status: 500,
638
- statusText: "Internal Server Error"
639
- })
640
- );
641
- const err = await fetchBytes("bafyabc", "https://gw/ipfs/").catch((e) => e);
642
- expect(err).toBeInstanceOf(BulletinGatewayFetchError);
643
- expect(err.cid).toBe("bafyabc");
644
- expect(err.status).toBe(500);
645
- });
646
- test("throws on timeout", async () => {
647
- vi.stubGlobal(
648
- "fetch",
649
- vi.fn().mockImplementation(
650
- (_url, init) => new Promise((_resolve, reject) => {
651
- init.signal.addEventListener(
652
- "abort",
653
- () => reject(new DOMException("aborted", "AbortError"))
654
- );
655
- })
656
- )
657
- );
658
- await expect(
659
- fetchBytes("bafyabc", "https://gw/ipfs/", { timeoutMs: 10 })
660
- ).rejects.toThrow();
661
- });
662
- });
663
- describe("fetchJson", () => {
664
- test("parses JSON from response", async () => {
665
- const obj = { name: "test", value: 42 };
666
- const bytes = new TextEncoder().encode(JSON.stringify(obj));
667
- vi.stubGlobal(
668
- "fetch",
669
- vi.fn().mockResolvedValue({
670
- ok: true,
671
- arrayBuffer: () => Promise.resolve(bytes.buffer)
672
- })
673
- );
674
- const result = await fetchJson(
675
- "bafyabc",
676
- "https://gw/ipfs/"
677
- );
678
- expect(result).toEqual(obj);
679
- });
680
- });
205
+ function bytesToHex(bytes) {
206
+ let s = "";
207
+ for (let i = 0; i < bytes.length; i++) {
208
+ s += bytes[i].toString(16).padStart(2, "0");
209
+ }
210
+ return s;
681
211
  }
682
212
 
683
- // src/query.ts
684
- import { createLogger as createLogger4 } from "@parity/product-sdk-logger";
685
-
686
213
  // src/resolve-query.ts
687
- import { getPreimageManager } from "@parity/product-sdk-host";
688
- import { createLogger as createLogger3 } from "@parity/product-sdk-logger";
689
- var log3 = createLogger3("bulletin");
214
+ var log2 = createLogger("bulletin");
690
215
  var DEFAULT_LOOKUP_TIMEOUT_MS = 3e4;
691
216
  async function resolveQueryStrategy() {
692
217
  const preimageManager = await getPreimageManager();
693
218
  if (preimageManager) {
694
- log3.info("using host preimage lookup for bulletin queries");
219
+ log2.info("using host preimage lookup for bulletin queries");
695
220
  return {
696
221
  kind: "host-lookup",
697
222
  lookup: (cid, timeoutMs) => lookupViaHost(preimageManager, cid, timeoutMs)
@@ -730,84 +255,9 @@ function lookupViaHost(manager, cid, timeoutMs = DEFAULT_LOOKUP_TIMEOUT_MS) {
730
255
  });
731
256
  });
732
257
  }
733
- if (void 0) {
734
- const { describe, test, expect, vi } = void 0;
735
- describe("lookupViaHost", () => {
736
- function createMockManager(behavior) {
737
- const unsubscribe = vi.fn();
738
- const cancelInterrupt = vi.fn();
739
- let interruptCb;
740
- const lookup = vi.fn((_key, callback) => {
741
- const data = new Uint8Array([10, 20, 30]);
742
- queueMicrotask(() => {
743
- if (behavior === "resolve") {
744
- callback(data);
745
- } else if (behavior === "null-then-resolve") {
746
- callback(null);
747
- queueMicrotask(() => callback(data));
748
- } else if (behavior === "interrupt") {
749
- interruptCb?.();
750
- }
751
- });
752
- return {
753
- unsubscribe,
754
- onInterrupt: (cb) => {
755
- interruptCb = cb;
756
- return cancelInterrupt;
757
- }
758
- };
759
- });
760
- return { lookup, unsubscribe, cancelInterrupt, submit: vi.fn() };
761
- }
762
- const testCid = computeCid2(new TextEncoder().encode("test"));
763
- test("resolves on first non-null callback", async () => {
764
- const manager = createMockManager("resolve");
765
- const result = await lookupViaHost(manager, testCid);
766
- expect(result).toEqual(new Uint8Array([10, 20, 30]));
767
- });
768
- test("ignores null callbacks and resolves on subsequent data", async () => {
769
- const manager = createMockManager("null-then-resolve");
770
- const result = await lookupViaHost(manager, testCid);
771
- expect(result).toEqual(new Uint8Array([10, 20, 30]));
772
- });
773
- test("rejects with BulletinLookupTimeoutError on timeout", async () => {
774
- const { BulletinLookupTimeoutError: BulletinLookupTimeoutError2 } = await null;
775
- const manager = createMockManager("hang");
776
- const err = await lookupViaHost(manager, testCid, 50).catch((e) => e);
777
- expect(err).toBeInstanceOf(BulletinLookupTimeoutError2);
778
- expect(err.cid).toBe(testCid);
779
- expect(err.timeoutMs).toBe(50);
780
- });
781
- test("rejects with BulletinLookupInterruptedError on interrupt", async () => {
782
- const { BulletinLookupInterruptedError: BulletinLookupInterruptedError2 } = await null;
783
- const manager = createMockManager("interrupt");
784
- const err = await lookupViaHost(manager, testCid).catch((e) => e);
785
- expect(err).toBeInstanceOf(BulletinLookupInterruptedError2);
786
- expect(err.cid).toBe(testCid);
787
- });
788
- test("calls unsubscribe and cancelInterrupt on resolution", async () => {
789
- const manager = createMockManager("resolve");
790
- await lookupViaHost(manager, testCid);
791
- expect(manager.unsubscribe).toHaveBeenCalledOnce();
792
- expect(manager.cancelInterrupt).toHaveBeenCalledOnce();
793
- });
794
- test("calls unsubscribe on interrupt", async () => {
795
- const manager = createMockManager("interrupt");
796
- await lookupViaHost(manager, testCid).catch(() => {
797
- });
798
- expect(manager.unsubscribe).toHaveBeenCalledOnce();
799
- });
800
- test("passes correct hex key to manager", async () => {
801
- const expectedKey = cidToPreimageKey(testCid);
802
- const manager = createMockManager("resolve");
803
- await lookupViaHost(manager, testCid);
804
- expect(manager.lookup).toHaveBeenCalledWith(expectedKey, expect.any(Function));
805
- });
806
- });
807
- }
808
258
 
809
259
  // src/query.ts
810
- var log4 = createLogger4("bulletin");
260
+ var log3 = createLogger("bulletin");
811
261
  async function queryBytes(cid, options) {
812
262
  const strategy = await resolveQueryStrategy();
813
263
  return executeQuery(strategy, cid, options);
@@ -817,528 +267,268 @@ async function queryJson(cid, options) {
817
267
  return JSON.parse(new TextDecoder().decode(bytes));
818
268
  }
819
269
  async function executeQuery(strategy, cid, options) {
820
- log4.info("querying via host preimage lookup", { cid });
821
- return strategy.lookup(cid, options?.lookupTimeoutMs);
270
+ log3.info("query: host preimage lookup", { cid });
271
+ const bytes = await strategy.lookup(cid, options?.lookupTimeoutMs);
272
+ if (options?.noReassemble) return bytes;
273
+ const parsed = parseCid(cid);
274
+ if (parsed.code !== CidCodec$1.DagPb) return bytes;
275
+ log3.info("query: reassembling DAG-PB manifest", { cid });
276
+ const builder = new UnixFsDagBuilder();
277
+ const { chunkCids } = await builder.parse(bytes);
278
+ const chunks = await Promise.all(
279
+ chunkCids.map((c) => strategy.lookup(c.toString(), options?.lookupTimeoutMs))
280
+ );
281
+ let total = 0;
282
+ for (const chunk of chunks) total += chunk.length;
283
+ const out = new Uint8Array(total);
284
+ let offset = 0;
285
+ for (const chunk of chunks) {
286
+ out.set(chunk, offset);
287
+ offset += chunk.length;
288
+ }
289
+ return out;
822
290
  }
823
- if (void 0) {
824
- const { describe, test, expect, vi } = void 0;
825
- describe("executeQuery", () => {
826
- const testData = new Uint8Array([1, 2, 3]);
827
- test("executes host-lookup strategy", async () => {
828
- const lookup = vi.fn().mockResolvedValue(testData);
829
- const strategy = { kind: "host-lookup", lookup };
830
- const result = await executeQuery(strategy, "bafytest");
831
- expect(result).toBe(testData);
832
- expect(lookup).toHaveBeenCalledWith("bafytest", void 0);
833
- });
834
- test("passes lookupTimeoutMs to host-lookup", async () => {
835
- const lookup = vi.fn().mockResolvedValue(testData);
836
- const strategy = { kind: "host-lookup", lookup };
837
- await executeQuery(strategy, "bafytest", { lookupTimeoutMs: 5e3 });
838
- expect(lookup).toHaveBeenCalledWith("bafytest", 5e3);
839
- });
840
- });
841
- }
842
-
843
- // src/upload.ts
844
- import { createLogger as createLogger6 } from "@parity/product-sdk-logger";
845
- import { submitAndWatch, withRetry } from "@parity/product-sdk-tx";
846
- import { Binary } from "polkadot-api";
847
-
848
- // src/resolve-signer.ts
849
- import { getPreimageManager as getPreimageManager2 } from "@parity/product-sdk-host";
850
- import { createLogger as createLogger5 } from "@parity/product-sdk-logger";
851
- var log5 = createLogger5("bulletin");
852
- async function resolveUploadStrategy(explicitSigner) {
853
- if (explicitSigner) {
854
- log5.debug("using explicit signer provided by caller");
855
- return { kind: "signer", signer: explicitSigner };
856
- }
857
- const preimageManager = await getPreimageManager2();
858
- if (preimageManager) {
859
- log5.info("using host preimage API for bulletin upload");
860
- return { kind: "preimage", submit: (data) => preimageManager.submit(data) };
291
+ var HASH_CODE_TO_ENUM_TYPE = {
292
+ [HashAlgorithm.Blake2b256]: "Blake2b256",
293
+ [HashAlgorithm.Sha2_256]: "Sha2_256",
294
+ [HashAlgorithm.Keccak256]: "Keccak256"
295
+ };
296
+ async function verifyOnChain(api, cid, options) {
297
+ const parsed = parseCidForVerify(cid);
298
+ const queryFn = api.query?.TransactionStorage?.Transactions?.getValue;
299
+ if (!queryFn) {
300
+ throw new Error(
301
+ "Bulletin API does not expose query.TransactionStorage.Transactions \u2014 the typed API may be incomplete or the runtime version doesn't match the descriptor."
302
+ );
861
303
  }
862
- throw new BulletinHostUnavailableError("upload");
863
- }
864
- if (void 0) {
865
- const { describe, test, expect, vi } = void 0;
866
- describe("resolveUploadStrategy", () => {
867
- test("returns explicit signer when provided", async () => {
868
- const signer = { publicKey: new Uint8Array(32) };
869
- const strategy = await resolveUploadStrategy(signer);
870
- expect(strategy.kind).toBe("signer");
871
- if (strategy.kind === "signer") {
872
- expect(strategy.signer).toBe(signer);
873
- }
874
- });
875
- });
876
- }
877
-
878
- // src/upload.ts
879
- var log6 = createLogger6("bulletin");
880
- async function upload(api, data, signer, options) {
881
- const strategy = await resolveUploadStrategy(signer);
882
- const cid = computeCid(data);
883
- if (strategy.kind === "preimage") {
884
- log6.info("uploading via host preimage API", { cid, size: data.byteLength });
885
- const preimageKey = await strategy.submit(data);
886
- log6.info("preimage submitted successfully", { cid, preimageKey });
887
- return {
888
- kind: "preimage",
889
- cid,
890
- preimageKey,
891
- gatewayUrl: options?.gateway ? gatewayUrl(cid, options.gateway) : void 0
892
- };
304
+ const entries = await queryFn(options.block);
305
+ if (!entries || entries.length === 0) return null;
306
+ if (options.index !== void 0) {
307
+ const entry = entries[options.index];
308
+ if (entry && matchesEntry(entry, parsed)) {
309
+ return {
310
+ block: options.block,
311
+ index: options.index,
312
+ size: entry.size,
313
+ blockChunks: entry.block_chunks
314
+ };
315
+ }
316
+ return null;
893
317
  }
894
- log6.info("uploading via TransactionStorage.store", { cid, size: data.byteLength });
895
- const result = await withRetry(() => {
896
- const tx = api.tx.TransactionStorage.store({ data: Binary.fromBytes(data) });
897
- return submitAndWatch(tx, strategy.signer, {
898
- waitFor: options?.waitFor,
899
- timeoutMs: options?.timeoutMs,
900
- onStatus: options?.onStatus
901
- });
902
- });
903
- log6.info("transaction included in block", { cid, blockHash: result.block.hash });
904
- return {
905
- kind: "transaction",
906
- cid,
907
- blockHash: result.block.hash,
908
- gatewayUrl: options?.gateway ? gatewayUrl(cid, options.gateway) : void 0
909
- };
910
- }
911
- async function batchUpload(api, items, signer, options) {
912
- if (items.length === 0) return [];
913
- const strategy = await resolveUploadStrategy(signer);
914
- const results = [];
915
- for (let i = 0; i < items.length; i++) {
916
- const item = items[i];
917
- const cid = computeCid(item.data);
918
- try {
919
- if (strategy.kind === "preimage") {
920
- log6.info("batch: uploading item via preimage", {
921
- label: item.label,
922
- index: i,
923
- total: items.length
924
- });
925
- const preimageKey = await strategy.submit(item.data);
926
- const entry = {
927
- kind: "preimage",
928
- label: item.label,
929
- cid,
930
- success: true,
931
- preimageKey,
932
- gatewayUrl: options?.gateway ? gatewayUrl(cid, options.gateway) : void 0
933
- };
934
- results.push(entry);
935
- options?.onProgress?.(i + 1, items.length, entry);
936
- } else {
937
- log6.info("batch: uploading item via transaction", {
938
- label: item.label,
939
- index: i,
940
- total: items.length
941
- });
942
- const result = await withRetry(() => {
943
- const tx = api.tx.TransactionStorage.store({
944
- data: Binary.fromBytes(item.data)
945
- });
946
- return submitAndWatch(tx, strategy.signer, {
947
- waitFor: options?.waitFor,
948
- timeoutMs: options?.timeoutMs
949
- });
950
- });
951
- const entry = {
952
- kind: "transaction",
953
- label: item.label,
954
- cid,
955
- success: true,
956
- blockHash: result.block.hash,
957
- gatewayUrl: options?.gateway ? gatewayUrl(cid, options.gateway) : void 0
958
- };
959
- results.push(entry);
960
- options?.onProgress?.(i + 1, items.length, entry);
961
- }
962
- } catch (err) {
963
- log6.error("batch: item upload failed", {
964
- label: item.label,
318
+ for (let i = 0; i < entries.length; i++) {
319
+ const entry = entries[i];
320
+ if (matchesEntry(entry, parsed)) {
321
+ return {
322
+ block: options.block,
965
323
  index: i,
966
- error: err instanceof Error ? err.message : String(err)
967
- });
968
- const entry = {
969
- kind: strategy.kind === "preimage" ? "preimage" : "transaction",
970
- label: item.label,
971
- cid,
972
- success: false,
973
- error: err instanceof Error ? err.message : String(err)
324
+ size: entry.size,
325
+ blockChunks: entry.block_chunks
974
326
  };
975
- results.push(entry);
976
- options?.onProgress?.(i + 1, items.length, entry);
977
327
  }
978
328
  }
979
- return results;
329
+ return null;
980
330
  }
981
- if (void 0) {
982
- let createMockApi = function() {
983
- return {
984
- tx: {
985
- TransactionStorage: {
986
- store: vi.fn().mockReturnValue({
987
- signSubmitAndWatch: () => ({
988
- subscribe: (handlers) => {
989
- queueMicrotask(() => {
990
- handlers.next({ type: "signed", txHash: "0xtxhash" });
991
- handlers.next({
992
- type: "txBestBlocksState",
993
- txHash: "0xtxhash",
994
- found: true,
995
- ok: true,
996
- block: { hash: "0xblockhash", number: 1, index: 0 },
997
- events: []
998
- });
999
- });
1000
- return { unsubscribe: vi.fn() };
1001
- }
1002
- })
1003
- })
1004
- }
1005
- }
1006
- };
1007
- };
1008
- createMockApi2 = createMockApi;
1009
- const { describe, test, expect, vi } = void 0;
1010
- const mockSigner = {};
1011
- describe("upload", () => {
1012
- test("calls TransactionStorage.store and returns CID + blockHash with explicit signer", async () => {
1013
- const api = createMockApi();
1014
- const data = new TextEncoder().encode("test data");
1015
- const result = await upload(api, data, mockSigner);
1016
- expect(api.tx.TransactionStorage.store).toHaveBeenCalledOnce();
1017
- expect(result.kind).toBe("transaction");
1018
- expect(result.cid).toBeTruthy();
1019
- if (result.kind === "transaction") {
1020
- expect(result.blockHash).toBe("0xblockhash");
1021
- }
1022
- });
1023
- test("includes gatewayUrl when gateway option provided", async () => {
1024
- const api = createMockApi();
1025
- const data = new TextEncoder().encode("test");
1026
- const result = await upload(api, data, mockSigner, {
1027
- gateway: "https://gw/ipfs/"
1028
- });
1029
- expect(result.gatewayUrl).toBe(`https://gw/ipfs/${result.cid}`);
1030
- });
1031
- test("omits gatewayUrl when no gateway option", async () => {
1032
- const api = createMockApi();
1033
- const data = new TextEncoder().encode("test");
1034
- const result = await upload(api, data, mockSigner);
1035
- expect(result.gatewayUrl).toBeUndefined();
1036
- });
1037
- });
1038
- describe("batchUpload", () => {
1039
- test("returns empty array for empty items", async () => {
1040
- const api = createMockApi();
1041
- const results = await batchUpload(api, [], mockSigner);
1042
- expect(results).toEqual([]);
1043
- });
1044
- test("processes items sequentially with explicit signer", async () => {
1045
- const api = createMockApi();
1046
- const items = [
1047
- { data: new TextEncoder().encode("a"), label: "file-a" },
1048
- { data: new TextEncoder().encode("b"), label: "file-b" }
1049
- ];
1050
- const results = await batchUpload(api, items, mockSigner);
1051
- expect(results).toHaveLength(2);
1052
- expect(results[0].kind).toBe("transaction");
1053
- expect(results[0].label).toBe("file-a");
1054
- expect(results[0].success).toBe(true);
1055
- expect(results[1].kind).toBe("transaction");
1056
- expect(results[1].label).toBe("file-b");
1057
- expect(results[1].success).toBe(true);
1058
- });
1059
- test("captures individual failures without aborting batch", async () => {
1060
- const api = createMockApi();
1061
- let callCount = 0;
1062
- api.tx.TransactionStorage.store.mockImplementation(() => {
1063
- callCount++;
1064
- if (callCount === 2) {
1065
- return {
1066
- signSubmitAndWatch: () => ({
1067
- subscribe: (handlers) => {
1068
- queueMicrotask(() => {
1069
- handlers.next({ type: "signed", txHash: "0x" });
1070
- handlers.next({
1071
- type: "txBestBlocksState",
1072
- txHash: "0x",
1073
- found: true,
1074
- ok: false,
1075
- block: { hash: "0xblock", number: 1, index: 0 },
1076
- events: [],
1077
- dispatchError: { type: "BadOrigin" }
1078
- });
1079
- });
1080
- return { unsubscribe: vi.fn() };
1081
- }
1082
- })
1083
- };
1084
- }
1085
- return {
1086
- signSubmitAndWatch: () => ({
1087
- subscribe: (handlers) => {
1088
- queueMicrotask(() => {
1089
- handlers.next({ type: "signed", txHash: "0x" });
1090
- handlers.next({
1091
- type: "txBestBlocksState",
1092
- txHash: "0x",
1093
- found: true,
1094
- ok: true,
1095
- block: { hash: "0xblock", number: 1, index: 0 },
1096
- events: []
1097
- });
1098
- });
1099
- return { unsubscribe: vi.fn() };
1100
- }
1101
- })
1102
- };
1103
- });
1104
- const items = [
1105
- { data: new TextEncoder().encode("a"), label: "ok" },
1106
- { data: new TextEncoder().encode("b"), label: "fail" },
1107
- { data: new TextEncoder().encode("c"), label: "ok2" }
1108
- ];
1109
- const results = await batchUpload(api, items, mockSigner);
1110
- expect(results).toHaveLength(3);
1111
- expect(results[0].kind).toBe("transaction");
1112
- expect(results[0].success).toBe(true);
1113
- const f1 = results[1];
1114
- expect(f1.kind).toBe("transaction");
1115
- expect(f1.success).toBe(false);
1116
- if (!f1.success) expect(f1.error).toContain("BadOrigin");
1117
- expect(results[2].success).toBe(true);
1118
- });
1119
- test("calls onProgress for each item", async () => {
1120
- const api = createMockApi();
1121
- const items = [
1122
- { data: new TextEncoder().encode("a"), label: "a" },
1123
- { data: new TextEncoder().encode("b"), label: "b" }
1124
- ];
1125
- const progress = [];
1126
- await batchUpload(api, items, mockSigner, {
1127
- onProgress: (done, total, current) => progress.push([done, total, current.label])
1128
- });
1129
- expect(progress).toEqual([
1130
- [1, 2, "a"],
1131
- [2, 2, "b"]
1132
- ]);
1133
- });
1134
- });
331
+ function parseCidForVerify(cid) {
332
+ let parsed;
333
+ try {
334
+ parsed = CID.parse(cid);
335
+ } catch {
336
+ throw new BulletinCidError(`Invalid CID: ${cid}`, cid);
337
+ }
338
+ if (parsed.version !== 1) {
339
+ throw new BulletinCidError(`Expected CIDv1, got CIDv${parsed.version}`, cid);
340
+ }
341
+ const hashType = HASH_CODE_TO_ENUM_TYPE[parsed.multihash.code];
342
+ if (!hashType) {
343
+ throw new BulletinCidError(
344
+ `Unsupported hash algorithm 0x${parsed.multihash.code.toString(16)}`,
345
+ cid
346
+ );
347
+ }
348
+ return { digest: parsed.multihash.digest, hashType };
349
+ }
350
+ function matchesEntry(entry, target) {
351
+ if (entry.hashing.type !== target.hashType) return false;
352
+ const onChainBytes = entry.content_hash instanceof Uint8Array ? entry.content_hash : entry.content_hash.asBytes();
353
+ if (onChainBytes.length !== target.digest.length) return false;
354
+ for (let i = 0; i < onChainBytes.length; i++) {
355
+ if (onChainBytes[i] !== target.digest[i]) return false;
356
+ }
357
+ return true;
1135
358
  }
1136
- var createMockApi2;
1137
359
 
1138
360
  // src/client.ts
361
+ var log4 = createLogger("bulletin");
1139
362
  var BulletinClient = class _BulletinClient {
363
+ /** Underlying upstream client — exposed for power users. */
364
+ inner;
365
+ /** Typed Bulletin Chain API. */
1140
366
  api;
1141
- gateway;
367
+ /** Lazy-resolved host-preimage query strategy, cached for the client lifetime. */
1142
368
  queryStrategyPromise = null;
1143
- constructor(api, gateway) {
369
+ /** Constructed via {@link create} or {@link from}. */
370
+ constructor(inner, api) {
371
+ this.inner = inner;
1144
372
  this.api = api;
1145
- this.gateway = gateway;
1146
373
  }
1147
- /** Lazily resolve and cache the query strategy for the client lifetime. */
374
+ /** Resolve and cache the host query strategy on first use. */
1148
375
  resolveQuery() {
1149
376
  if (!this.queryStrategyPromise) {
1150
377
  this.queryStrategyPromise = resolveQueryStrategy();
1151
378
  }
1152
379
  return this.queryStrategyPromise;
1153
380
  }
1154
- /** Create from an environment — resolves API via chain-client, gateway from known list. */
1155
- static async create(env) {
1156
- const chain = await getChainAPI(env);
1157
- return new _BulletinClient(chain.bulletin, getGateway(env));
1158
- }
1159
- /** Create from an explicit API and gateway (custom setups, testing). */
1160
- static from(api, gateway) {
1161
- return new _BulletinClient(api, gateway);
1162
- }
1163
- /** Compute CID without uploading. Static — no instance needed. */
1164
- static computeCid(data) {
1165
- return computeCid(data);
1166
- }
1167
381
  /**
1168
- * Reconstruct a CID from a `0x`-prefixed hex hash. Static no instance needed.
382
+ * Create a client from an environment shorthand or an explicit network.
1169
383
  *
1170
- * Useful for converting on-chain hashes back to CIDs for IPFS gateway lookups.
1171
- * Pass optional hash algorithm and codec to match the on-chain CID configuration.
384
+ * Environment form uses our `getChainAPI(env)` to resolve the typed API.
385
+ * Explicit form skips the environment lookup and lets you pass any
386
+ * genesis/descriptor combo.
1172
387
  *
1173
- * @see {@link hashToCid} for full documentation.
1174
- */
1175
- static hashToCid(hexHash, hashCode, codec) {
1176
- return hashToCid(hexHash, hashCode, codec);
1177
- }
1178
- /**
1179
- * Upload data to the Bulletin Chain.
388
+ * @example
389
+ * ```ts
390
+ * // Shorthand
391
+ * const client = await BulletinClient.create({ environment: "paseo", signer });
1180
392
  *
1181
- * @param data - Raw bytes to store.
1182
- * @param signer - Optional signer. When omitted, uses the host preimage API.
1183
- * @param options - Upload options (timeout, waitFor, status callback).
393
+ * // Explicit (custom network)
394
+ * const client = await BulletinClient.create({
395
+ * ...BulletinChain.paseo,
396
+ * signer,
397
+ * config: { defaultChunkSize: 1 << 20 },
398
+ * });
399
+ * ```
1184
400
  */
1185
- async upload(data, signer, options) {
1186
- return upload(this.api, data, signer, { ...options, gateway: this.gateway });
401
+ static async create(options) {
402
+ if ("environment" in options) {
403
+ const chain2 = await getChainAPI(options.environment);
404
+ const inner2 = new AsyncBulletinClient(
405
+ chain2.bulletin,
406
+ options.signer,
407
+ chain2.raw.bulletin.submit,
408
+ options.config,
409
+ () => chain2.destroy()
410
+ );
411
+ log4.info("BulletinClient created (environment shorthand)", {
412
+ environment: options.environment
413
+ });
414
+ return new _BulletinClient(inner2, chain2.bulletin);
415
+ }
416
+ const { genesisHash, descriptor, signer, config } = options;
417
+ if (descriptor.genesis && genesisHash.toLowerCase() !== descriptor.genesis.toLowerCase()) {
418
+ throw new Error(
419
+ `BulletinClient.create: genesisHash (${genesisHash}) does not match descriptor.genesis (${descriptor.genesis}). These must refer to the same network \u2014 check that you're pairing the right descriptor with the right genesis hash.`
420
+ );
421
+ }
422
+ const chain = await createChainClient({
423
+ chains: { bulletin: descriptor },
424
+ rpcs: { bulletin: [] }
425
+ });
426
+ const inner = new AsyncBulletinClient(
427
+ chain.bulletin,
428
+ signer,
429
+ chain.raw.bulletin.submit,
430
+ config,
431
+ () => chain.destroy()
432
+ );
433
+ log4.info("BulletinClient created (explicit network)");
434
+ return new _BulletinClient(inner, chain.bulletin);
1187
435
  }
1188
436
  /**
1189
- * Upload multiple items sequentially.
437
+ * Construct from a pre-built `AsyncBulletinClient` and PAPI typed API.
1190
438
  *
1191
- * @param items - Array of items to upload, each with data and a label.
1192
- * @param signer - Optional signer. When omitted, auto-resolved.
1193
- * @param options - Batch upload options (timeout, progress callback).
439
+ * Use this when you already own the connection lifecycle (BYOD setups,
440
+ * tests). The caller is responsible for calling `papiClient.destroy()`
441
+ * this client's {@link destroy} only tears down the upstream's
442
+ * `onDestroy` hook.
1194
443
  */
1195
- async batchUpload(items, signer, options) {
1196
- return batchUpload(this.api, items, signer, { ...options, gateway: this.gateway });
444
+ static from(inner, api) {
445
+ return new _BulletinClient(inner, api);
1197
446
  }
447
+ // ─── Upload + authorization (forwarded to upstream) ────────────────
448
+ /** Build a store transaction. See upstream `StoreBuilder` for chained options. */
449
+ store(data) {
450
+ return this.inner.store(data);
451
+ }
452
+ /** Authorize an account to store data on the chain (sudo required on most networks). */
453
+ authorizeAccount(who, transactions, bytes) {
454
+ return this.inner.authorizeAccount(who, transactions, bytes);
455
+ }
456
+ /** Authorize content storage by hash (anyone can store; no fees). */
457
+ authorizePreimage(contentHash, maxSize) {
458
+ return this.inner.authorizePreimage(contentHash, maxSize);
459
+ }
460
+ /** Renew a stored transaction by block + index. */
461
+ renew(block, index) {
462
+ return this.inner.renew(block, index);
463
+ }
464
+ /** Estimate the authorization (transactions + bytes) needed for `dataSize`. */
465
+ estimateAuthorization(dataSize) {
466
+ return this.inner.estimateAuthorization(dataSize);
467
+ }
468
+ // ─── Read side (our own helpers) ───────────────────────────────────
1198
469
  /**
1199
- * Fetch raw bytes by CID.
470
+ * Fetch raw bytes for a CID via the host's preimage lookup.
1200
471
  *
1201
- * Uses host preimage lookup with caching.
472
+ * Container-only outside a Polkadot Browser / Desktop host this
473
+ * throws {@link BulletinHostUnavailableError}. The chain stores
474
+ * content metadata (`content_hash`, size, codec) but the bytes
475
+ * themselves are surfaced through the host's preimage subscription.
476
+ *
477
+ * Use {@link verifyOnChain} if you only need to confirm a CID was
478
+ * recorded on-chain (no byte fetch).
1202
479
  */
1203
480
  async fetchBytes(cid, options) {
1204
481
  const strategy = await this.resolveQuery();
1205
482
  return executeQuery(strategy, cid, options);
1206
483
  }
1207
- /**
1208
- * Fetch and parse JSON by CID.
1209
- *
1210
- * Auto-resolves query path (same as {@link fetchBytes}).
1211
- */
484
+ /** Fetch and parse JSON for a CID. */
1212
485
  async fetchJson(cid, options) {
1213
486
  const bytes = await this.fetchBytes(cid, options);
1214
487
  return JSON.parse(new TextDecoder().decode(bytes));
1215
488
  }
1216
- /** Check if a CID exists on the gateway. */
1217
- async cidExists(cid) {
1218
- return cidExists(cid, this.gateway);
1219
- }
1220
- /** Build the full gateway URL for a CID. */
1221
- gatewayUrl(cid) {
1222
- return gatewayUrl(cid, this.gateway);
489
+ /** Pre-flight: check whether `address` can store on the bulletin chain. */
490
+ async checkAuthorization(address) {
491
+ return checkAuthorization(this.api, address);
1223
492
  }
1224
493
  /**
1225
- * Check whether an account is authorized to store data on the Bulletin Chain.
1226
- *
1227
- * Use as a pre-flight check before {@link upload} to provide clear UX
1228
- * instead of letting the transaction fail mid-execution.
494
+ * Verify that a CID was recorded on-chain at the given block.
1229
495
  *
1230
- * @param address - SS58-encoded account address to check.
1231
- * @returns Authorization status with remaining quota.
1232
- *
1233
- * @see {@link checkAuthorization} for the standalone function equivalent.
496
+ * Common pattern: pass `blockNumber` (and optionally `extrinsicIndex`)
497
+ * from a `store(...).send()` receipt to confirm the upload landed.
498
+ * See {@link verifyOnChain} for details.
1234
499
  */
1235
- async checkAuthorization(address) {
1236
- return checkAuthorization(this.api, address);
500
+ async verifyOnChain(cid, options) {
501
+ return verifyOnChain(this.api, cid, options);
502
+ }
503
+ /** Tear down the underlying connection. */
504
+ async destroy() {
505
+ await this.inner.destroy();
1237
506
  }
1238
507
  };
1239
- if (void 0) {
1240
- const { describe, test, expect, vi } = void 0;
1241
- const mockApi = {
1242
- tx: {
1243
- TransactionStorage: {
1244
- store: vi.fn().mockReturnValue({
1245
- signSubmitAndWatch: () => ({
1246
- subscribe: (handlers) => {
1247
- queueMicrotask(() => {
1248
- handlers.next({ type: "signed", txHash: "0x" });
1249
- handlers.next({
1250
- type: "txBestBlocksState",
1251
- txHash: "0x",
1252
- found: true,
1253
- ok: true,
1254
- block: { hash: "0xblock", number: 1, index: 0 },
1255
- events: []
1256
- });
1257
- });
1258
- return { unsubscribe: vi.fn() };
1259
- }
1260
- })
1261
- })
1262
- }
1263
- }
508
+ var BulletinChain = {
509
+ paseo: {
510
+ genesisHash: "0x744960c32e3a3df5440e1ecd4d34096f1ce2230d7016a5ada8a765d5a622b4ea",
511
+ descriptor: bulletin
512
+ }
513
+ };
514
+
515
+ // src/lazy-signer.ts
516
+ function createLazySigner(getSigner, onMissing = "No signer available \u2014 connect a wallet and select an account first.") {
517
+ const resolve = () => {
518
+ const inner = getSigner();
519
+ if (!inner) throw new Error(onMissing);
520
+ return inner;
1264
521
  };
1265
- const GATEWAY = "https://test-gw/ipfs/";
1266
- describe("BulletinClient", () => {
1267
- test("from() creates client with given API and gateway", () => {
1268
- const client = BulletinClient.from(mockApi, GATEWAY);
1269
- expect(client.api).toBe(mockApi);
1270
- expect(client.gateway).toBe(GATEWAY);
1271
- });
1272
- test("computeCid() is static and delegates to standalone", () => {
1273
- const data = new TextEncoder().encode("hello");
1274
- const cid = BulletinClient.computeCid(data);
1275
- expect(cid).toBe(computeCid(data));
1276
- });
1277
- test("hashToCid() is static and delegates to standalone", () => {
1278
- const data = new TextEncoder().encode("hello");
1279
- const cid = computeCid(data);
1280
- const key = cidToPreimageKey2(cid);
1281
- expect(BulletinClient.hashToCid(key)).toBe(cid);
1282
- });
1283
- test("gatewayUrl() returns gateway + cid", () => {
1284
- const client = BulletinClient.from(mockApi, GATEWAY);
1285
- expect(client.gatewayUrl("bafyabc")).toBe("https://test-gw/ipfs/bafyabc");
1286
- });
1287
- test("upload() passes gateway from client with explicit signer", async () => {
1288
- const client = BulletinClient.from(mockApi, GATEWAY);
1289
- const data = new TextEncoder().encode("test");
1290
- const result = await client.upload(data, {});
1291
- expect(result.gatewayUrl).toContain(GATEWAY);
1292
- expect(result.cid).toBeTruthy();
1293
- });
1294
- test("checkAuthorization delegates to standalone", async () => {
1295
- const authMockApi = {
1296
- ...mockApi,
1297
- query: {
1298
- TransactionStorage: {
1299
- Authorizations: {
1300
- getValue: vi.fn().mockResolvedValue({
1301
- extent: { transactions: 5, bytes: 2000n },
1302
- expiration: 100
1303
- })
1304
- }
1305
- }
1306
- }
1307
- };
1308
- const client = BulletinClient.from(authMockApi, GATEWAY);
1309
- const status = await client.checkAuthorization("5GrwvaEF...");
1310
- expect(status.authorized).toBe(true);
1311
- expect(status.remainingTransactions).toBe(5);
1312
- expect(status.remainingBytes).toBe(2000n);
1313
- });
1314
- });
522
+ const lazy = {
523
+ get publicKey() {
524
+ return resolve().publicKey;
525
+ },
526
+ signTx: async (...args) => resolve().signTx(...args),
527
+ signBytes: async (...args) => resolve().signBytes(...args)
528
+ };
529
+ return lazy;
1315
530
  }
1316
- export {
1317
- BulletinAuthorizationError,
1318
- BulletinCidError,
1319
- BulletinClient,
1320
- BulletinError,
1321
- BulletinGatewayFetchError,
1322
- BulletinGatewayUnavailableError,
1323
- BulletinHostUnavailableError,
1324
- BulletinLookupInterruptedError,
1325
- BulletinLookupTimeoutError,
1326
- CidCodec,
1327
- HashAlgorithm,
1328
- batchUpload,
1329
- checkAuthorization,
1330
- cidExists,
1331
- cidToPreimageKey,
1332
- computeCid,
1333
- fetchBytes,
1334
- fetchJson,
1335
- gatewayUrl,
1336
- getGateway,
1337
- hashToCid,
1338
- queryBytes,
1339
- queryJson,
1340
- resolveQueryStrategy,
1341
- resolveUploadStrategy,
1342
- upload
1343
- };
531
+
532
+ export { BulletinAuthorizationError, BulletinChain, BulletinCidError, BulletinClient, BulletinHostUnavailableError, BulletinLookupInterruptedError, BulletinLookupTimeoutError, CidCodec, HashAlgorithm, ProductBulletinError, authorizeAccount, checkAuthorization, cidToPreimageKey, createLazySigner, executeQuery, hashToCid, queryBytes, queryJson, resolveQueryStrategy, verifyOnChain };
533
+ //# sourceMappingURL=index.js.map
1344
534
  //# sourceMappingURL=index.js.map