@polkadot-apps/statement-store 0.2.4 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +421 -0
  2. package/package.json +5 -4
package/README.md ADDED
@@ -0,0 +1,421 @@
1
+ # @polkadot-apps/statement-store
2
+
3
+ Publish/subscribe client for the Polkadot Statement Store with topic-based routing and SCALE encoding.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pnpm add @polkadot-apps/statement-store
9
+ ```
10
+
11
+ This package depends on `@polkadot-apps/chain-client`, `@polkadot-apps/descriptors`, `@polkadot-apps/logger`, `@noble/hashes`, and `polkadot-api`, which are installed automatically.
12
+
13
+ ## Quick start
14
+
15
+ ```typescript
16
+ import { StatementStoreClient } from "@polkadot-apps/statement-store";
17
+
18
+ const client = new StatementStoreClient({ appName: "my-app" });
19
+ await client.connect(signer);
20
+
21
+ // Publish a message
22
+ await client.publish({ type: "hello", peerId: "abc" }, {
23
+ channel: "presence/abc",
24
+ topic2: "room-123",
25
+ });
26
+
27
+ // Subscribe to messages
28
+ const sub = client.subscribe<{ type: string }>(statement => {
29
+ console.log(statement.data.type);
30
+ });
31
+
32
+ // Clean up
33
+ sub.unsubscribe();
34
+ client.destroy();
35
+ ```
36
+
37
+ ## StatementStoreClient
38
+
39
+ The primary interface for publishing and subscribing to statements. Handles SCALE encoding, Sr25519 signing, topic management, and resilient delivery via subscription with polling fallback.
40
+
41
+ ### Creating a client
42
+
43
+ ```typescript
44
+ import { StatementStoreClient } from "@polkadot-apps/statement-store";
45
+
46
+ const client = new StatementStoreClient({
47
+ appName: "my-app", // Required. Used as primary topic (blake2b hash).
48
+ endpoint: "wss://rpc.example.com", // Optional. Fallback WebSocket endpoint.
49
+ pollIntervalMs: 10_000, // Optional. Polling interval. Default: 10000.
50
+ defaultTtlSeconds: 30, // Optional. Statement TTL. Default: 30.
51
+ enablePolling: true, // Optional. Enable polling fallback. Default: true.
52
+ });
53
+ ```
54
+
55
+ ### Connecting
56
+
57
+ Call `connect` with an Sr25519 signer to establish the transport. The client tries the chain-client bulletin first, then falls back to the configured WebSocket endpoint.
58
+
59
+ ```typescript
60
+ import type { StatementSignerWithKey } from "@polkadot-apps/statement-store";
61
+
62
+ const signer: StatementSignerWithKey = {
63
+ publicKey: myPublicKey, // Uint8Array, 32 bytes
64
+ sign: (msg) => mySignFn(msg), // Returns Uint8Array (64 bytes) or Promise<Uint8Array>
65
+ };
66
+
67
+ await client.connect(signer);
68
+ console.log(client.isConnected()); // true
69
+ console.log(client.getPublicKeyHex()); // "0xaa..."
70
+ ```
71
+
72
+ ### Publishing
73
+
74
+ Publish typed JSON data. Returns `true` if the network accepted the statement ("new" or "known"), `false` if rejected.
75
+
76
+ ```typescript
77
+ const accepted = await client.publish(
78
+ { type: "presence", peerId: "abc", timestamp: Date.now() },
79
+ {
80
+ channel: "presence/abc", // Optional. Enables last-write-wins deduplication.
81
+ topic2: "room-123", // Optional. Secondary topic for subscriber filtering.
82
+ ttlSeconds: 60, // Optional. Overrides default TTL.
83
+ decryptionKey: keyBytes, // Optional. 32-byte hint for filtering.
84
+ },
85
+ );
86
+ ```
87
+
88
+ Data is serialized as JSON and encoded to UTF-8. The maximum payload size is 512 bytes.
89
+
90
+ ### Subscribing
91
+
92
+ Listen for incoming statements in real time. Statements are deduplicated by channel and expiry.
93
+
94
+ ```typescript
95
+ const sub = client.subscribe<{ type: string; peerId: string }>(
96
+ (statement) => {
97
+ console.log(statement.data.type);
98
+ console.log(statement.signer); // Uint8Array | undefined
99
+ console.log(statement.channel); // Uint8Array | undefined
100
+ console.log(statement.expiry); // bigint | undefined
101
+ },
102
+ { topic2: "room-123" }, // Optional. Filter by secondary topic.
103
+ );
104
+
105
+ // Stop listening
106
+ sub.unsubscribe();
107
+ ```
108
+
109
+ ### Querying existing statements
110
+
111
+ Fetch statements that were published before the subscription started.
112
+
113
+ ```typescript
114
+ const statements = await client.query<{ type: string }>({
115
+ topic2: "room-123",
116
+ });
117
+
118
+ for (const stmt of statements) {
119
+ console.log(stmt.data, stmt.signer);
120
+ }
121
+ ```
122
+
123
+ ### Cleanup
124
+
125
+ ```typescript
126
+ client.destroy(); // Stops polling, unsubscribes, closes transport. Safe to call multiple times.
127
+ ```
128
+
129
+ ## ChannelStore
130
+
131
+ A higher-level abstraction providing last-write-wins semantics over `StatementStoreClient`. Each named channel holds a single value; newer writes replace older ones by timestamp.
132
+
133
+ ```typescript
134
+ import { ChannelStore } from "@polkadot-apps/statement-store";
135
+
136
+ interface Presence {
137
+ type: "presence";
138
+ peerId: string;
139
+ timestamp: number;
140
+ }
141
+
142
+ const channels = new ChannelStore<Presence>(client, { topic2: "doc-123" });
143
+
144
+ // Write
145
+ await channels.write("presence/peer-abc", {
146
+ type: "presence",
147
+ peerId: "abc",
148
+ timestamp: Date.now(),
149
+ });
150
+
151
+ // Read a single channel
152
+ const value = channels.read("presence/peer-abc"); // Presence | undefined
153
+
154
+ // Read all channels
155
+ for (const [hashKey, value] of channels.readAll()) {
156
+ console.log(value.peerId);
157
+ }
158
+
159
+ // Track the number of active channels
160
+ console.log(channels.size);
161
+
162
+ // React to changes
163
+ const sub = channels.onChange((channelKey, value, previous) => {
164
+ console.log(`Updated: ${channelKey}`, value, previous);
165
+ });
166
+
167
+ sub.unsubscribe();
168
+ channels.destroy();
169
+ ```
170
+
171
+ If the written value lacks a `timestamp` field, one is added automatically using `Date.now()`.
172
+
173
+ ## Topic and channel utilities
174
+
175
+ ```typescript
176
+ import {
177
+ createTopic,
178
+ createChannel,
179
+ topicToHex,
180
+ topicsEqual,
181
+ serializeTopicFilter,
182
+ } from "@polkadot-apps/statement-store";
183
+
184
+ const topic = createTopic("my-app"); // TopicHash (blake2b-256)
185
+ const channel = createChannel("presence"); // ChannelHash (blake2b-256)
186
+
187
+ const hex = topicToHex(topic); // "0x..."
188
+ const equal = topicsEqual(topicA, topicB); // boolean
189
+
190
+ const serialized = serializeTopicFilter({ matchAll: [topic] });
191
+ // { matchAll: ["0x..."] }
192
+ ```
193
+
194
+ ## Codec (advanced)
195
+
196
+ Direct access to SCALE encoding and decoding for consumers that need low-level control.
197
+
198
+ ```typescript
199
+ import {
200
+ encodeData,
201
+ decodeData,
202
+ encodeStatement,
203
+ decodeStatement,
204
+ createSignatureMaterial,
205
+ toHex,
206
+ fromHex,
207
+ } from "@polkadot-apps/statement-store";
208
+
209
+ // Encode/decode JSON payloads
210
+ const bytes = encodeData({ hello: "world" }); // Uint8Array (JSON to UTF-8, max 512 bytes)
211
+ const parsed = decodeData<{ hello: string }>(bytes);
212
+
213
+ // Encode/decode full SCALE statements
214
+ const encoded = encodeStatement(fields, signerPublicKey, signature); // Uint8Array
215
+ const decoded = decodeStatement("0x..."); // DecodedStatement
216
+
217
+ // Signature material for manual signing
218
+ const material = createSignatureMaterial(fields); // Uint8Array
219
+
220
+ // Hex conversion
221
+ const hex = toHex(new Uint8Array([0xde, 0xad])); // "0xdead"
222
+ const raw = fromHex("0xdead"); // Uint8Array
223
+ ```
224
+
225
+ ## Transport (advanced)
226
+
227
+ For consumers that need custom transport implementations or direct WebSocket access.
228
+
229
+ ```typescript
230
+ import { createTransport, RpcTransport } from "@polkadot-apps/statement-store";
231
+
232
+ // Auto-detect: tries chain-client bulletin, falls back to direct WebSocket
233
+ const transport = await createTransport({ endpoint: "wss://rpc.example.com" });
234
+
235
+ // Or use the RPC transport directly
236
+ const rpc = new RpcTransport(/* ... */);
237
+ ```
238
+
239
+ ## Constants
240
+
241
+ | Constant | Value | Description |
242
+ |----------|-------|-------------|
243
+ | `MAX_STATEMENT_SIZE` | `512` | Maximum data payload size in bytes |
244
+ | `MAX_USER_TOTAL` | `1024` | Maximum total storage per user in bytes |
245
+ | `DEFAULT_TTL_SECONDS` | `30` | Default statement time-to-live in seconds |
246
+ | `DEFAULT_POLL_INTERVAL_MS` | `10000` | Default polling interval in milliseconds |
247
+
248
+ ## Error handling
249
+
250
+ All errors extend `StatementStoreError`. Catch the base class to handle any error from this package.
251
+
252
+ ```typescript
253
+ import {
254
+ StatementStoreError,
255
+ StatementEncodingError,
256
+ StatementSubmitError,
257
+ StatementSubscriptionError,
258
+ StatementConnectionError,
259
+ StatementDataTooLargeError,
260
+ } from "@polkadot-apps/statement-store";
261
+
262
+ try {
263
+ await client.publish(data);
264
+ } catch (err) {
265
+ if (err instanceof StatementDataTooLargeError) {
266
+ console.error(`Too large: ${err.actualSize}/${err.maxSize} bytes`);
267
+ } else if (err instanceof StatementConnectionError) {
268
+ console.error("Not connected");
269
+ } else if (err instanceof StatementStoreError) {
270
+ console.error("Statement store error:", err.message);
271
+ }
272
+ }
273
+ ```
274
+
275
+ | Error class | When it is thrown | Extra properties |
276
+ |-------------|-------------------|------------------|
277
+ | `StatementEncodingError` | SCALE encode/decode failed | -- |
278
+ | `StatementSubmitError` | Node rejected the statement | `detail: unknown` |
279
+ | `StatementSubscriptionError` | Subscription setup failed (non-fatal) | -- |
280
+ | `StatementConnectionError` | Transport connection failed | -- |
281
+ | `StatementDataTooLargeError` | Data exceeds 512 bytes | `actualSize: number`, `maxSize: number` |
282
+
283
+ ## API
284
+
285
+ ### StatementStoreClient
286
+
287
+ ```typescript
288
+ class StatementStoreClient {
289
+ constructor(config: StatementStoreConfig)
290
+ connect(signer: StatementSignerWithKey): Promise<void>
291
+ publish<T>(data: T, options?: PublishOptions): Promise<boolean>
292
+ subscribe<T>(callback: (statement: ReceivedStatement<T>) => void, options?: { topic2?: string }): Unsubscribable
293
+ query<T>(options?: { topic2?: string; decryptionKey?: Uint8Array }): Promise<ReceivedStatement<T>[]>
294
+ isConnected(): boolean
295
+ getPublicKeyHex(): string
296
+ destroy(): void
297
+ }
298
+ ```
299
+
300
+ ### ChannelStore
301
+
302
+ ```typescript
303
+ class ChannelStore<T extends { timestamp?: number }> {
304
+ constructor(client: StatementStoreClient, options?: { topic2?: string })
305
+ write(channelName: string, value: T): Promise<boolean>
306
+ read(channelName: string): T | undefined
307
+ readAll(): ReadonlyMap<string, T>
308
+ get size(): number
309
+ onChange(callback: (channelName: string, value: T, previous: T | undefined) => void): Unsubscribable
310
+ destroy(): void
311
+ }
312
+ ```
313
+
314
+ ### Topic/channel utilities
315
+
316
+ ```typescript
317
+ function createTopic(name: string): TopicHash
318
+ function createChannel(name: string): ChannelHash
319
+ function topicToHex(hash: Uint8Array): string
320
+ function topicsEqual(a: Uint8Array, b: Uint8Array): boolean
321
+ function serializeTopicFilter(filter: TopicFilter): SerializedTopicFilter
322
+ ```
323
+
324
+ ### Codec
325
+
326
+ ```typescript
327
+ function encodeStatement(fields: StatementFields, signer: Uint8Array, signature: Uint8Array): Uint8Array
328
+ function decodeStatement(hex: string): DecodedStatement
329
+ function encodeData<T>(value: T): Uint8Array
330
+ function decodeData<T>(bytes: Uint8Array): T
331
+ function createSignatureMaterial(fields: StatementFields): Uint8Array
332
+ function toHex(bytes: Uint8Array): string
333
+ function fromHex(hex: string): Uint8Array
334
+ ```
335
+
336
+ ### Transport
337
+
338
+ ```typescript
339
+ function createTransport(config: { endpoint?: string }): Promise<StatementTransport>
340
+ class RpcTransport { /* JSON-RPC over WebSocket */ }
341
+ ```
342
+
343
+ ## Types
344
+
345
+ ```typescript
346
+ /** Branded 32-byte blake2b-256 hash for statement topics. */
347
+ type TopicHash = Uint8Array & { readonly __brand: "TopicHash" };
348
+
349
+ /** Branded 32-byte blake2b-256 hash for statement channels. */
350
+ type ChannelHash = Uint8Array & { readonly __brand: "ChannelHash" };
351
+
352
+ interface StatementStoreConfig {
353
+ appName: string;
354
+ endpoint?: string;
355
+ pollIntervalMs?: number; // Default: 10000
356
+ defaultTtlSeconds?: number; // Default: 30
357
+ enablePolling?: boolean; // Default: true
358
+ }
359
+
360
+ interface PublishOptions {
361
+ channel?: string;
362
+ topic2?: string;
363
+ ttlSeconds?: number;
364
+ decryptionKey?: Uint8Array;
365
+ }
366
+
367
+ interface ReceivedStatement<T = unknown> {
368
+ data: T;
369
+ signer?: Uint8Array;
370
+ channel?: Uint8Array;
371
+ topic1?: Uint8Array;
372
+ topic2?: Uint8Array;
373
+ expiry?: bigint;
374
+ raw: DecodedStatement;
375
+ }
376
+
377
+ interface StatementFields {
378
+ expirationTimestamp: number;
379
+ sequenceNumber: number;
380
+ decryptionKey?: Uint8Array;
381
+ channel?: Uint8Array;
382
+ topic1?: Uint8Array;
383
+ topic2?: Uint8Array;
384
+ data?: Uint8Array;
385
+ }
386
+
387
+ interface DecodedStatement {
388
+ signer?: Uint8Array;
389
+ expiry?: bigint;
390
+ decryptionKey?: Uint8Array;
391
+ topic1?: Uint8Array;
392
+ topic2?: Uint8Array;
393
+ channel?: Uint8Array;
394
+ data?: Uint8Array;
395
+ }
396
+
397
+ type TopicFilter = "any" | { matchAll: TopicHash[] } | { matchAny: TopicHash[] };
398
+ type SerializedTopicFilter = "any" | { matchAll: string[] } | { matchAny: string[] };
399
+
400
+ interface StatementSignerWithKey {
401
+ publicKey: Uint8Array;
402
+ sign: (message: Uint8Array) => Uint8Array | Promise<Uint8Array>;
403
+ }
404
+
405
+ type SubmitStatus = "new" | "known" | "rejected";
406
+
407
+ interface Unsubscribable {
408
+ unsubscribe: () => void;
409
+ }
410
+
411
+ interface StatementTransport {
412
+ subscribe(filter: TopicFilter, onStatement: (hex: string) => void, onError: (error: Error) => void): Unsubscribable;
413
+ submit(statementHex: string): Promise<SubmitStatus>;
414
+ query(topics: TopicHash[], decryptionKey?: string): Promise<string[]>;
415
+ destroy(): void;
416
+ }
417
+ ```
418
+
419
+ ## License
420
+
421
+ Apache-2.0
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@polkadot-apps/statement-store",
3
- "version": "0.2.4",
3
+ "description": "Publish/subscribe client for the Polkadot Statement Store with topic-based routing and SCALE encoding",
4
+ "version": "0.2.5",
4
5
  "type": "module",
5
6
  "main": "./dist/index.js",
6
7
  "types": "./dist/index.d.ts",
@@ -19,9 +20,9 @@
19
20
  "dependencies": {
20
21
  "@noble/hashes": "^2.0.1",
21
22
  "polkadot-api": "^1.23.3",
22
- "@polkadot-apps/chain-client": "0.3.5",
23
- "@polkadot-apps/descriptors": "0.1.6",
24
- "@polkadot-apps/logger": "0.1.3"
23
+ "@polkadot-apps/chain-client": "0.3.6",
24
+ "@polkadot-apps/descriptors": "0.1.7",
25
+ "@polkadot-apps/logger": "0.1.4"
25
26
  },
26
27
  "devDependencies": {
27
28
  "typescript": "^5.9.3"