@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.
- package/README.md +421 -0
- 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
|
-
"
|
|
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.
|
|
23
|
-
"@polkadot-apps/descriptors": "0.1.
|
|
24
|
-
"@polkadot-apps/logger": "0.1.
|
|
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"
|