@parity/product-sdk-bulletin 0.1.0 → 0.2.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.
package/src/errors.ts CHANGED
@@ -1,33 +1,42 @@
1
1
  /**
2
- * Base class for all Bulletin Chain errors.
2
+ * Bulletin error types.
3
3
  *
4
- * Use `instanceof BulletinError` to catch any bulletin-related error.
4
+ * Two error families coexist:
5
5
  *
6
- * @example
7
- * ```ts
8
- * try {
9
- * await bulletin.upload(data);
10
- * } catch (e) {
11
- * if (e instanceof BulletinError) {
12
- * console.error("Bulletin operation failed:", e.message);
13
- * }
14
- * }
15
- * ```
6
+ * 1. **Upstream SDK errors** — `BulletinError` and `ErrorCode` from
7
+ * `@parity/bulletin-sdk` cover upload/store/authorization failures
8
+ * surfaced by `AsyncBulletinClient`. Each carries `code`, `retryable`,
9
+ * and `recoveryHint`.
10
+ * 2. **Read-side errors** declared here — host preimage availability /
11
+ * lookup timeouts / interrupts, plus CID format problems, surfaced by
12
+ * our retrieval helpers (`fetchBytes`, `fetchJson`, `verifyOnChain`).
13
+ *
14
+ * Catch upstream errors with `instanceof BulletinError`. Catch our read-side
15
+ * errors with `instanceof ProductBulletinError` (or the specific subclass).
16
+ */
17
+ export { BulletinError, ErrorCode } from "@parity/bulletin-sdk";
18
+
19
+ /**
20
+ * Base class for read-side errors raised by `@parity/product-sdk-bulletin`.
21
+ *
22
+ * Distinct from upstream `BulletinError` which covers upload/store failures.
16
23
  */
17
- export class BulletinError extends Error {
24
+ export class ProductBulletinError extends Error {
18
25
  constructor(message: string, options?: ErrorOptions) {
19
26
  super(message, options);
20
- this.name = "BulletinError";
27
+ this.name = "ProductBulletinError";
21
28
  }
22
29
  }
23
30
 
24
31
  /**
25
32
  * The host preimage API is unavailable.
26
33
  *
27
- * Thrown when bulletin operations require the host container but it's not available.
28
- * This typically means the SDK is running outside of Polkadot Browser/Desktop.
34
+ * Thrown when bulletin operations require the host container but it's not
35
+ * available. This typically means the SDK is running outside Polkadot
36
+ * Browser / Desktop. The bulletin SDK is container-only by design — see
37
+ * the README for the rationale.
29
38
  */
30
- export class BulletinHostUnavailableError extends BulletinError {
39
+ export class BulletinHostUnavailableError extends ProductBulletinError {
31
40
  constructor(operation: "upload" | "query") {
32
41
  super(
33
42
  `Host preimage API unavailable for ${operation}. Ensure you are running inside a host container (Polkadot Browser / Desktop).`,
@@ -39,10 +48,10 @@ export class BulletinHostUnavailableError extends BulletinError {
39
48
  /**
40
49
  * The host preimage lookup timed out.
41
50
  *
42
- * The host was unable to retrieve the requested content within the timeout period.
43
- * The content may still become available later.
51
+ * The host was unable to retrieve the requested content within the timeout
52
+ * period. The content may still become available later.
44
53
  */
45
- export class BulletinLookupTimeoutError extends BulletinError {
54
+ export class BulletinLookupTimeoutError extends ProductBulletinError {
46
55
  /** The CID that was being looked up. */
47
56
  readonly cid: string;
48
57
  /** The timeout duration in milliseconds. */
@@ -59,10 +68,10 @@ export class BulletinLookupTimeoutError extends BulletinError {
59
68
  /**
60
69
  * The host interrupted the preimage lookup.
61
70
  *
62
- * The host terminated the lookup subscription, typically after repeated failures
63
- * or when the host determines the content is unavailable.
71
+ * The host terminated the lookup subscription, typically after repeated
72
+ * failures or when the host determines the content is unavailable.
64
73
  */
65
- export class BulletinLookupInterruptedError extends BulletinError {
74
+ export class BulletinLookupInterruptedError extends ProductBulletinError {
66
75
  /** The CID that was being looked up. */
67
76
  readonly cid: string;
68
77
 
@@ -73,13 +82,27 @@ export class BulletinLookupInterruptedError extends BulletinError {
73
82
  }
74
83
  }
75
84
 
85
+ /**
86
+ * Invalid CID format or version.
87
+ */
88
+ export class BulletinCidError extends ProductBulletinError {
89
+ /** The invalid CID string, if available. */
90
+ readonly cid?: string;
91
+
92
+ constructor(message: string, cid?: string) {
93
+ super(message);
94
+ this.name = "BulletinCidError";
95
+ this.cid = cid;
96
+ }
97
+ }
98
+
76
99
  /**
77
100
  * Failed to check authorization status for an account.
78
101
  *
79
102
  * Wraps RPC or query errors that occur when checking if an account
80
103
  * is authorized to store data on the Bulletin Chain.
81
104
  */
82
- export class BulletinAuthorizationError extends BulletinError {
105
+ export class BulletinAuthorizationError extends ProductBulletinError {
83
106
  /** The address that was being checked. */
84
107
  readonly address: string;
85
108
 
@@ -90,146 +113,50 @@ export class BulletinAuthorizationError extends BulletinError {
90
113
  }
91
114
  }
92
115
 
93
- /**
94
- * The IPFS gateway for the specified environment is not available.
95
- *
96
- * Thrown when attempting to use gateway features for a network that
97
- * doesn't have a live bulletin gateway yet.
98
- */
99
- export class BulletinGatewayUnavailableError extends BulletinError {
100
- /** The environment that was requested. */
101
- readonly environment: string;
102
-
103
- constructor(environment: string) {
104
- super(`Bulletin gateway for "${environment}" is not yet available`);
105
- this.name = "BulletinGatewayUnavailableError";
106
- this.environment = environment;
107
- }
108
- }
109
-
110
- /**
111
- * An IPFS gateway request failed.
112
- *
113
- * Thrown when a fetch to the IPFS gateway returns a non-OK response.
114
- */
115
- export class BulletinGatewayFetchError extends BulletinError {
116
- /** The CID that was being fetched. */
117
- readonly cid: string;
118
- /** The HTTP status code returned by the gateway. */
119
- readonly status: number;
120
- /** The HTTP status text returned by the gateway. */
121
- readonly statusText: string;
122
-
123
- constructor(cid: string, status: number, statusText: string) {
124
- super(`Gateway fetch failed for ${cid}: ${status} ${statusText}`);
125
- this.name = "BulletinGatewayFetchError";
126
- this.cid = cid;
127
- this.status = status;
128
- this.statusText = statusText;
129
- }
130
- }
131
-
132
- /**
133
- * Invalid CID format or version.
134
- *
135
- * Thrown when a CID string cannot be parsed or has an unexpected version/codec.
136
- */
137
- export class BulletinCidError extends BulletinError {
138
- /** The invalid CID string, if available. */
139
- readonly cid?: string;
140
-
141
- constructor(message: string, cid?: string) {
142
- super(message);
143
- this.name = "BulletinCidError";
144
- this.cid = cid;
145
- }
146
- }
147
-
148
116
  if (import.meta.vitest) {
149
117
  const { describe, test, expect } = import.meta.vitest;
150
118
 
151
- describe("BulletinError hierarchy", () => {
152
- test("BulletinError is instanceof Error", () => {
153
- const err = new BulletinError("test");
119
+ describe("ProductBulletinError hierarchy", () => {
120
+ test("ProductBulletinError extends Error", () => {
121
+ const err = new ProductBulletinError("test");
154
122
  expect(err).toBeInstanceOf(Error);
155
- expect(err).toBeInstanceOf(BulletinError);
156
- expect(err.name).toBe("BulletinError");
123
+ expect(err.name).toBe("ProductBulletinError");
124
+ });
125
+
126
+ test("BulletinCidError", () => {
127
+ const err = new BulletinCidError("bad", "Qmabc");
128
+ expect(err).toBeInstanceOf(ProductBulletinError);
129
+ expect(err.cid).toBe("Qmabc");
157
130
  });
158
131
 
159
132
  test("BulletinHostUnavailableError", () => {
160
- const err = new BulletinHostUnavailableError("upload");
161
- expect(err).toBeInstanceOf(BulletinError);
162
- expect(err.name).toBe("BulletinHostUnavailableError");
163
- expect(err.message).toContain("upload");
133
+ const err = new BulletinHostUnavailableError("query");
134
+ expect(err).toBeInstanceOf(ProductBulletinError);
135
+ expect(err.message).toContain("query");
164
136
  expect(err.message).toContain("Host preimage API unavailable");
165
137
  });
166
138
 
167
139
  test("BulletinLookupTimeoutError", () => {
168
140
  const err = new BulletinLookupTimeoutError("bafyabc123", 30000);
169
- expect(err).toBeInstanceOf(BulletinError);
170
- expect(err.name).toBe("BulletinLookupTimeoutError");
141
+ expect(err).toBeInstanceOf(ProductBulletinError);
171
142
  expect(err.cid).toBe("bafyabc123");
172
143
  expect(err.timeoutMs).toBe(30000);
173
144
  expect(err.message).toContain("30000ms");
174
- expect(err.message).toContain("bafyabc123");
175
145
  });
176
146
 
177
147
  test("BulletinLookupInterruptedError", () => {
178
148
  const err = new BulletinLookupInterruptedError("bafyabc123");
179
- expect(err).toBeInstanceOf(BulletinError);
180
- expect(err.name).toBe("BulletinLookupInterruptedError");
149
+ expect(err).toBeInstanceOf(ProductBulletinError);
181
150
  expect(err.cid).toBe("bafyabc123");
182
151
  expect(err.message).toContain("interrupted");
183
152
  });
184
153
 
185
- test("BulletinAuthorizationError with cause", () => {
186
- const cause = new Error("RPC timeout");
154
+ test("BulletinAuthorizationError carries cause", () => {
155
+ const cause = new Error("RPC down");
187
156
  const err = new BulletinAuthorizationError("5GrwvaEF...", cause);
188
- expect(err).toBeInstanceOf(BulletinError);
189
- expect(err.name).toBe("BulletinAuthorizationError");
157
+ expect(err).toBeInstanceOf(ProductBulletinError);
190
158
  expect(err.address).toBe("5GrwvaEF...");
191
159
  expect(err.cause).toBe(cause);
192
160
  });
193
-
194
- test("BulletinGatewayUnavailableError", () => {
195
- const err = new BulletinGatewayUnavailableError("polkadot");
196
- expect(err).toBeInstanceOf(BulletinError);
197
- expect(err.name).toBe("BulletinGatewayUnavailableError");
198
- expect(err.environment).toBe("polkadot");
199
- expect(err.message).toContain("polkadot");
200
- });
201
-
202
- test("BulletinGatewayFetchError", () => {
203
- const err = new BulletinGatewayFetchError("bafyabc", 404, "Not Found");
204
- expect(err).toBeInstanceOf(BulletinError);
205
- expect(err.name).toBe("BulletinGatewayFetchError");
206
- expect(err.cid).toBe("bafyabc");
207
- expect(err.status).toBe(404);
208
- expect(err.statusText).toBe("Not Found");
209
- expect(err.message).toContain("404");
210
- });
211
-
212
- test("BulletinCidError", () => {
213
- const err = new BulletinCidError("Expected CIDv1, got CIDv0", "Qmabc");
214
- expect(err).toBeInstanceOf(BulletinError);
215
- expect(err.name).toBe("BulletinCidError");
216
- expect(err.cid).toBe("Qmabc");
217
- });
218
-
219
- test("all errors can be caught with BulletinError", () => {
220
- const errors = [
221
- new BulletinHostUnavailableError("query"),
222
- new BulletinLookupTimeoutError("cid", 1000),
223
- new BulletinLookupInterruptedError("cid"),
224
- new BulletinAuthorizationError("addr"),
225
- new BulletinGatewayUnavailableError("env"),
226
- new BulletinGatewayFetchError("cid", 500, "Error"),
227
- new BulletinCidError("bad cid"),
228
- ];
229
-
230
- for (const err of errors) {
231
- expect(err).toBeInstanceOf(BulletinError);
232
- }
233
- });
234
161
  });
235
162
  }
package/src/index.ts CHANGED
@@ -1,38 +1,103 @@
1
+ /**
2
+ * @parity/product-sdk-bulletin — Upload and retrieve content on the Polkadot Bulletin Chain.
3
+ *
4
+ * Wraps `@parity/bulletin-sdk` (chunking, DAG-PB manifests, CID calculation,
5
+ * progress events) and adds:
6
+ *
7
+ * - **Network presets** via {@link BulletinChain}
8
+ * - **Read helpers** ({@link BulletinClient.fetchBytes} / {@link BulletinClient.fetchJson})
9
+ * - **Authorization pre-flight** via {@link checkAuthorization}
10
+ *
11
+ * @packageDocumentation
12
+ */
13
+
1
14
  export { BulletinClient } from "./client.js";
2
- export { checkAuthorization } from "./authorization.js";
15
+ export type { CreateBulletinClientOptions } from "./client.js";
16
+ export { BulletinChain } from "./networks.js";
17
+ export type { BulletinEnvironment, BulletinNetwork } from "./networks.js";
18
+ export { authorizeAccount, checkAuthorization } from "./authorization.js";
19
+ export type { AuthorizeAccountOptions } from "./authorization.js";
20
+ export { createLazySigner } from "./lazy-signer.js";
21
+ export { executeQuery, queryBytes, queryJson } from "./query.js";
22
+ export { resolveQueryStrategy } from "./resolve-query.js";
23
+ export type { QueryStrategy } from "./resolve-query.js";
24
+ export { verifyOnChain } from "./verify.js";
25
+ export type { ChainStoredEntry, VerifyOnChainOptions } from "./verify.js";
3
26
  export {
4
- computeCid,
5
27
  cidToPreimageKey,
6
28
  hashToCid,
7
29
  HashAlgorithm,
8
30
  CidCodec,
9
31
  } from "./cid.js";
32
+
33
+ // Errors — both upstream `BulletinError` and our read-side errors
10
34
  export {
11
35
  BulletinError,
12
- BulletinHostUnavailableError,
13
- BulletinLookupTimeoutError,
14
- BulletinLookupInterruptedError,
36
+ ErrorCode,
37
+ ProductBulletinError,
15
38
  BulletinAuthorizationError,
16
- BulletinGatewayUnavailableError,
17
- BulletinGatewayFetchError,
18
39
  BulletinCidError,
40
+ BulletinHostUnavailableError,
41
+ BulletinLookupInterruptedError,
42
+ BulletinLookupTimeoutError,
19
43
  } from "./errors.js";
20
- export { getGateway, gatewayUrl, cidExists, fetchBytes, fetchJson } from "./gateway.js";
21
- export { resolveQueryStrategy } from "./resolve-query.js";
22
- export { queryBytes, queryJson } from "./query.js";
23
- export { resolveUploadStrategy } from "./resolve-signer.js";
24
- export { upload, batchUpload } from "./upload.js";
44
+
45
+ // Types
25
46
  export type {
26
47
  AuthorizationStatus,
27
48
  BulletinApi,
28
49
  Environment,
29
- UploadOptions,
30
- UploadResult,
31
- BatchUploadItem,
32
- BatchUploadResult,
33
- BatchUploadOptions,
34
- FetchOptions,
35
50
  QueryOptions,
36
51
  } from "./types.js";
37
- export type { UploadStrategy } from "./resolve-signer.js";
38
- export type { QueryStrategy } from "./resolve-query.js";
52
+
53
+ // Re-exports from upstream SDK surface for power users + so consumers
54
+ // don't need a separate `@parity/bulletin-sdk` import.
55
+ export {
56
+ AsyncBulletinClient,
57
+ BulletinPreparer,
58
+ AuthCallBuilder,
59
+ CallBuilder,
60
+ StoreBuilder,
61
+ MockBulletinClient,
62
+ AuthorizationScope,
63
+ ChunkStatus,
64
+ TxStatus,
65
+ WaitFor,
66
+ MAX_CHUNK_SIZE,
67
+ MAX_FILE_SIZE,
68
+ DEFAULT_CHUNKER_CONFIG,
69
+ DEFAULT_CLIENT_CONFIG,
70
+ DEFAULT_STORE_OPTIONS,
71
+ calculateCid,
72
+ cidFromBytes,
73
+ cidToBytes,
74
+ convertCid,
75
+ estimateAuthorization,
76
+ getContentHash,
77
+ parseCid,
78
+ reassembleChunks,
79
+ resolveClientConfig,
80
+ validateChunkSize,
81
+ CID,
82
+ } from "@parity/bulletin-sdk";
83
+
84
+ export type {
85
+ BulletinClientInterface,
86
+ BulletinTypedApi,
87
+ Chunk,
88
+ ChunkDetails,
89
+ ChunkProgressEvent,
90
+ ChunkedStoreResult,
91
+ ChunkerConfig,
92
+ ClientConfig,
93
+ DagManifest,
94
+ MockClientConfig,
95
+ MockOperation,
96
+ ProgressCallback,
97
+ ProgressEvent,
98
+ StoreOptions,
99
+ StoreResult,
100
+ SubmitFn,
101
+ TransactionReceipt,
102
+ TransactionStatusEvent,
103
+ } from "@parity/bulletin-sdk";
@@ -0,0 +1,113 @@
1
+ import type { PolkadotSigner } from "polkadot-api";
2
+
3
+ /**
4
+ * Build a `PolkadotSigner` whose underlying signer is resolved on every call.
5
+ *
6
+ * `AsyncBulletinClient` takes a fixed `PolkadotSigner` at construction, but
7
+ * apps often build the bulletin client before any account is selected. This
8
+ * wrapper defers signer resolution: each call to `signTx` / `signBytes`
9
+ * invokes `getSigner()` and forwards to the result. If the getter returns
10
+ * `null`, calls throw with a clear message.
11
+ *
12
+ * The `publicKey` field is *also* resolved lazily — accessing it before a
13
+ * signer is available throws. This means callers that read `publicKey`
14
+ * eagerly will fail fast with the same error rather than seeing a stale
15
+ * key from a previously-selected account.
16
+ *
17
+ * Account changes between calls are picked up automatically: each sign
18
+ * resolves the current signer.
19
+ */
20
+ export function createLazySigner(
21
+ getSigner: () => PolkadotSigner | null,
22
+ onMissing = "No signer available — connect a wallet and select an account first.",
23
+ ): PolkadotSigner {
24
+ const resolve = (): PolkadotSigner => {
25
+ const inner = getSigner();
26
+ if (!inner) throw new Error(onMissing);
27
+ return inner;
28
+ };
29
+
30
+ // `async` on the methods is deliberate: it converts a "no signer" throw
31
+ // from `resolve()` into a rejected Promise. PolkadotSigner.signTx /
32
+ // signBytes are typed as returning Promises, and consumers expect a
33
+ // rejection rather than a synchronous escape on the failure path.
34
+ const lazy: PolkadotSigner = {
35
+ get publicKey() {
36
+ return resolve().publicKey;
37
+ },
38
+ signTx: async (...args: Parameters<PolkadotSigner["signTx"]>) => resolve().signTx(...args),
39
+ signBytes: async (...args: Parameters<PolkadotSigner["signBytes"]>) =>
40
+ resolve().signBytes(...args),
41
+ };
42
+ return lazy;
43
+ }
44
+
45
+ if (import.meta.vitest) {
46
+ const { describe, test, expect, vi } = import.meta.vitest;
47
+
48
+ function makeMockSigner(label: string): PolkadotSigner {
49
+ return {
50
+ publicKey: new TextEncoder().encode(label),
51
+ signTx: vi.fn().mockResolvedValue(new Uint8Array([1])),
52
+ signBytes: vi.fn().mockResolvedValue(new Uint8Array([2])),
53
+ };
54
+ }
55
+
56
+ describe("createLazySigner", () => {
57
+ test("publicKey throws when getter returns null", () => {
58
+ const lazy = createLazySigner(() => null);
59
+ expect(() => lazy.publicKey).toThrow("No signer available");
60
+ });
61
+
62
+ test("publicKey resolves through getter when signer is available", () => {
63
+ const inner = makeMockSigner("alice");
64
+ const lazy = createLazySigner(() => inner);
65
+ expect(lazy.publicKey).toBe(inner.publicKey);
66
+ });
67
+
68
+ test("signTx throws when getter returns null", async () => {
69
+ const lazy = createLazySigner(() => null);
70
+ await expect(lazy.signTx(new Uint8Array(), {}, new Uint8Array(), 0)).rejects.toThrow(
71
+ "No signer available",
72
+ );
73
+ });
74
+
75
+ test("signTx forwards to current signer", async () => {
76
+ const inner = makeMockSigner("alice");
77
+ const lazy = createLazySigner(() => inner);
78
+ const callData = new Uint8Array([0xaa, 0xbb]);
79
+ const signedExtensions = {};
80
+ const metadata = new Uint8Array([0xcc]);
81
+ const atBlock = 42;
82
+ const result = await lazy.signTx(callData, signedExtensions, metadata, atBlock);
83
+ expect(inner.signTx).toHaveBeenCalledWith(
84
+ callData,
85
+ signedExtensions,
86
+ metadata,
87
+ atBlock,
88
+ );
89
+ expect(result).toEqual(new Uint8Array([1]));
90
+ });
91
+
92
+ test("signBytes forwards to current signer", async () => {
93
+ const inner = makeMockSigner("bob");
94
+ const lazy = createLazySigner(() => inner);
95
+ const result = await lazy.signBytes(new Uint8Array([9]));
96
+ expect(inner.signBytes).toHaveBeenCalledWith(new Uint8Array([9]));
97
+ expect(result).toEqual(new Uint8Array([2]));
98
+ });
99
+
100
+ test("picks up account changes between calls", () => {
101
+ let active: PolkadotSigner | null = makeMockSigner("first");
102
+ const lazy = createLazySigner(() => active);
103
+ expect(lazy.publicKey).toEqual(new TextEncoder().encode("first"));
104
+ active = makeMockSigner("second");
105
+ expect(lazy.publicKey).toEqual(new TextEncoder().encode("second"));
106
+ });
107
+
108
+ test("custom error message", () => {
109
+ const lazy = createLazySigner(() => null, "select an account first");
110
+ expect(() => lazy.publicKey).toThrow("select an account first");
111
+ });
112
+ });
113
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Known Bulletin Chain networks.
3
+ *
4
+ * Pairs each environment with the genesis hash and the PAPI descriptor needed
5
+ * to construct an `AsyncBulletinClient`. Re-uses the descriptor exported by
6
+ * `@parity/product-sdk-descriptors/bulletin` — the bulletin descriptor is the
7
+ * same across all environments today, so the difference between entries is
8
+ * the genesis hash (and, downstream, the chain RPC URL).
9
+ */
10
+ import { bulletin as bulletinDescriptor } from "@parity/product-sdk-descriptors/bulletin";
11
+
12
+ export interface BulletinNetwork {
13
+ /** Genesis hash of the bulletin chain on this environment. */
14
+ genesisHash: `0x${string}`;
15
+ /** PAPI descriptor for typed API access. */
16
+ descriptor: typeof bulletinDescriptor;
17
+ }
18
+
19
+ /**
20
+ * Bulletin Chain network presets.
21
+ *
22
+ * Use these with {@link BulletinClient.create} when you want to be explicit
23
+ * about the network rather than passing an environment string. Reads go
24
+ * through the host's preimage subscription (container-only); no gateway
25
+ * URL is configured per network.
26
+ */
27
+ export const BulletinChain = {
28
+ paseo: {
29
+ genesisHash: "0x744960c32e3a3df5440e1ecd4d34096f1ce2230d7016a5ada8a765d5a622b4ea",
30
+ descriptor: bulletinDescriptor,
31
+ },
32
+ } as const satisfies Record<string, BulletinNetwork>;
33
+
34
+ /** Network keys with built-in presets in {@link BulletinChain}. */
35
+ export type BulletinEnvironment = keyof typeof BulletinChain;
36
+
37
+ if (import.meta.vitest) {
38
+ const { describe, test, expect } = import.meta.vitest;
39
+
40
+ describe("BulletinChain", () => {
41
+ test("paseo has a valid genesis hash", () => {
42
+ expect(BulletinChain.paseo.genesisHash).toMatch(/^0x[a-f0-9]{64}$/);
43
+ });
44
+
45
+ test("paseo descriptor has matching genesis", () => {
46
+ expect(BulletinChain.paseo.descriptor.genesis).toBe(BulletinChain.paseo.genesisHash);
47
+ });
48
+ });
49
+ }