@parity/product-sdk-statement-store 0.4.10 → 0.5.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/dist/index.d.ts CHANGED
@@ -1,5 +1,50 @@
1
- import { TopicFilter as TopicFilter$1, Statement } from '@novasamatech/sdk-statement';
2
- export { Proof, TopicFilter as SdkTopicFilter, SignedStatement, Statement, SubmitResult, UnsignedStatement } from '@novasamatech/sdk-statement';
1
+ import { Statement as Statement$1, SignedStatement as SignedStatement$1 } from '@parity/product-sdk-host';
2
+ export { StatementProof as Proof } from '@parity/product-sdk-host';
3
+
4
+ /**
5
+ * Statement Store value types and helpers.
6
+ *
7
+ * The statement shapes are **derived** from the in-house `@parity/truapi` wire
8
+ * types (re-exported by `@parity/product-sdk-host`) rather than hand-redefined,
9
+ * so changes to the protocol's statement definition propagate automatically.
10
+ * The single intentional difference is `data`: this SDK layer works with decoded
11
+ * `Uint8Array` bytes, whereas the wire type carries a hex string. {@link Proof}
12
+ * and {@link Topic} are re-exported verbatim. The host transport bridges the
13
+ * `data` field (and the ergonomic {@link TopicFilter}).
14
+ *
15
+ * @module
16
+ */
17
+
18
+ /** A `0x`-prefixed hex string. */
19
+ type Hex = `0x${string}`;
20
+
21
+ /**
22
+ * A record published to the Statement Store. Derived from the truapi wire
23
+ * {@link WireStatement}, overriding only `data` to carry decoded `Uint8Array`
24
+ * bytes instead of a hex string. Other fields (`topics`, `channel`,
25
+ * `decryptionKey`, `expiry`, `proof`) inherit from the wire type.
26
+ */
27
+ type Statement = Omit<Statement$1, "data"> & {
28
+ data?: Uint8Array;
29
+ };
30
+ /** A {@link Statement} before signing — the same fields, minus `proof`. */
31
+ type UnsignedStatement = Omit<Statement, "proof">;
32
+ /** A {@link Statement} with its `proof` attached and ready for submission. Derived from the truapi wire signed statement, with decoded `data`. */
33
+ type SignedStatement = Omit<SignedStatement$1, "data"> & {
34
+ data?: Uint8Array;
35
+ };
36
+ /**
37
+ * Filter for subscribing to statements.
38
+ *
39
+ * - `"any"` — match all statements.
40
+ * - `{ matchAll }` — statement must contain **every** listed topic.
41
+ * - `{ matchAny }` — statement must contain **at least one** listed topic.
42
+ */
43
+ type TopicFilter$1 = "any" | {
44
+ matchAll: Hex[];
45
+ } | {
46
+ matchAny: Hex[];
47
+ };
3
48
 
4
49
  /** A record published to the Statement Store. Every field is optional: a `data` payload, `topics` for routing, a `channel` for last-write-wins, an `expiry`, and a `proof` once signed. */
5
50
 
@@ -62,14 +107,16 @@ interface StatementSignerWithKey {
62
107
  /**
63
108
  * Credentials for connecting to the statement store.
64
109
  *
65
- * - **Host mode**: Inside a container, proof creation is delegated to the host API.
66
- * The `accountId` is a `[ss58Address, chainPrefix]` tuple from product-sdk.
110
+ * - **Host mode**: Inside a container, proof creation is delegated to the host
111
+ * via the RFC-10 sponsored path (`createProofAuthorized`) the host signs
112
+ * with the product's allowance account, so `accountId` is no longer required
113
+ * and is ignored if supplied (kept for backward compatibility).
67
114
  * - **Local mode**: Outside a container, statements are signed locally using the
68
115
  * provided Sr25519 signer.
69
116
  */
70
117
  type ConnectionCredentials = {
71
118
  mode: "host";
72
- accountId: [string, number];
119
+ accountId?: [string, number];
73
120
  } | {
74
121
  mode: "local";
75
122
  signer: StatementSignerWithKey;
@@ -464,7 +511,7 @@ declare function createChannel(name: string): ChannelHash;
464
511
  * @param hash - A 32-byte topic or channel hash.
465
512
  * @returns Hex string with "0x" prefix.
466
513
  */
467
- declare function topicToHex(hash: Uint8Array): string;
514
+ declare function topicToHex(hash: Uint8Array): `0x${string}`;
468
515
  /**
469
516
  * Compare two topic or channel hashes for byte equality.
470
517
  *
@@ -582,4 +629,4 @@ declare class StatementDataTooLargeError extends StatementStoreError {
582
629
  constructor(actualSize: number, maxSize?: number);
583
630
  }
584
631
 
585
- export { type ChannelHash, ChannelStore, type ConnectionCredentials, DEFAULT_TTL_SECONDS, MAX_STATEMENT_SIZE, MAX_USER_TOTAL, type PublishOptions, type ReceivedStatement, StatementConnectionError, StatementDataTooLargeError, StatementEncodingError, type StatementSigner, type StatementSignerWithKey, StatementStoreClient, type StatementStoreConfig, StatementStoreError, StatementSubmitError, StatementSubscriptionError, type StatementTransport, type TopicFilter, type TopicHash, type Unsubscribable, createChannel, createTopic, createTransport, decodeData, encodeData, fromHex, serializeTopicFilter, toHex, topicToHex, topicsEqual };
632
+ export { type ChannelHash, ChannelStore, type ConnectionCredentials, DEFAULT_TTL_SECONDS, MAX_STATEMENT_SIZE, MAX_USER_TOTAL, type PublishOptions, type ReceivedStatement, type TopicFilter$1 as SdkTopicFilter, type SignedStatement, type Statement, StatementConnectionError, StatementDataTooLargeError, StatementEncodingError, type StatementSigner, type StatementSignerWithKey, StatementStoreClient, type StatementStoreConfig, StatementStoreError, StatementSubmitError, StatementSubscriptionError, type StatementTransport, type TopicFilter, type TopicHash, type UnsignedStatement, type Unsubscribable, createChannel, createTopic, createTransport, decodeData, encodeData, fromHex, serializeTopicFilter, toHex, topicToHex, topicsEqual };
package/dist/index.js CHANGED
@@ -1,6 +1,5 @@
1
1
  import { createLogger } from '@parity/product-sdk-logger';
2
2
  import { blake2b256 } from '@parity/product-sdk-utils';
3
- import { createExpiry } from '@novasamatech/sdk-statement';
4
3
 
5
4
  // src/client.ts
6
5
 
@@ -127,10 +126,12 @@ var HostTransport = class {
127
126
  const hostFilter = sdkFilterToHost(filter);
128
127
  try {
129
128
  const sub = this.store.subscribe(hostFilter, (page) => {
130
- const converted = page.statements.map(
131
- hostSignedStatementToSdk
132
- );
133
- onStatements(converted);
129
+ onStatements(page.statements.map(hostSignedStatementToSdk));
130
+ });
131
+ sub.onInterrupt((reason) => {
132
+ const msg = reason instanceof Error ? reason.message : "Host ended the statement subscription";
133
+ log.warn("Host subscription interrupted", { error: msg });
134
+ onError(new StatementSubscriptionError(msg));
134
135
  });
135
136
  log.info("Host subscription active");
136
137
  return {
@@ -147,15 +148,12 @@ var HostTransport = class {
147
148
  async signAndSubmit(statement, credentials) {
148
149
  if (credentials.mode !== "host") {
149
150
  throw new StatementConnectionError(
150
- "HostTransport requires host credentials. Use { mode: 'host', accountId } to connect."
151
+ "HostTransport requires host credentials. Use { mode: 'host' } to connect."
151
152
  );
152
153
  }
153
154
  const hostStatement = sdkStatementToHost(statement);
154
- const proof = await this.store.createProof(credentials.accountId, hostStatement);
155
- const signedStatement = {
156
- ...hostStatement,
157
- proof
158
- };
155
+ const proof = await this.store.createProofAuthorized(hostStatement);
156
+ const signedStatement = { ...hostStatement, proof };
159
157
  await this.store.submit(signedStatement);
160
158
  log.debug("Statement submitted via host");
161
159
  }
@@ -178,78 +176,32 @@ function sdkFilterToHost(filter) {
178
176
  return { matchAny: [] };
179
177
  }
180
178
  if ("matchAll" in filter) {
181
- return { matchAll: filter.matchAll.map(hexToBytes) };
179
+ return { matchAll: filter.matchAll };
182
180
  }
183
- return { matchAny: filter.matchAny.map(hexToBytes) };
181
+ return { matchAny: filter.matchAny };
184
182
  }
185
183
  function hostSignedStatementToSdk(hostStmt) {
186
- const result = {};
187
- if (hostStmt.data) result.data = hostStmt.data;
188
- if (hostStmt.expiry !== void 0) result.expiry = hostStmt.expiry;
189
- if (hostStmt.topics) {
190
- result.topics = hostStmt.topics.map(bytesToHex);
191
- }
192
- if (hostStmt.channel) {
193
- result.channel = bytesToHex(hostStmt.channel);
194
- }
195
- if (hostStmt.decryptionKey) {
196
- result.decryptionKey = bytesToHex(hostStmt.decryptionKey);
197
- }
198
- if (hostStmt.proof) {
199
- const tag = hostStmt.proof.tag;
200
- const value = hostStmt.proof.value;
201
- const sdkType = tag === "OnChain" ? "onChain" : tag.toLowerCase();
202
- if (sdkType === "onChain" && "who" in value) {
203
- result.proof = {
204
- type: "onChain",
205
- value: {
206
- who: bytesToHex(value.who),
207
- blockHash: bytesToHex(value.blockHash),
208
- event: value.event
209
- }
210
- };
211
- } else if ("signature" in value && "signer" in value) {
212
- result.proof = {
213
- type: sdkType,
214
- value: {
215
- signature: bytesToHex(value.signature),
216
- signer: bytesToHex(value.signer)
217
- }
218
- };
219
- }
220
- }
221
- return result;
184
+ return {
185
+ ...hostStmt,
186
+ data: hostStmt.data !== void 0 ? fromHex(hostStmt.data) : void 0
187
+ };
222
188
  }
223
189
  function sdkStatementToHost(stmt) {
224
- const result = {};
225
- if (stmt.data) result.data = stmt.data;
226
- if (stmt.expiry !== void 0) result.expiry = stmt.expiry;
227
- if (stmt.topics) {
228
- result.topics = stmt.topics.map(hexToBytes);
229
- }
230
- if (stmt.channel) {
231
- result.channel = hexToBytes(stmt.channel);
232
- }
233
- if (stmt.decryptionKey) {
234
- result.decryptionKey = hexToBytes(stmt.decryptionKey);
235
- }
236
- return result;
190
+ return {
191
+ ...stmt,
192
+ data: stmt.data !== void 0 ? toHex(stmt.data) : void 0
193
+ };
237
194
  }
238
- function hexToBytes(hex) {
239
- const clean = hex.startsWith("0x") ? hex.slice(2) : hex;
240
- const bytes = new Uint8Array(clean.length / 2);
241
- for (let i = 0; i < bytes.length; i++) {
242
- bytes[i] = Number.parseInt(clean.substring(i * 2, i * 2 + 2), 16);
243
- }
244
- return bytes;
245
- }
246
- function bytesToHex(bytes) {
247
- let hex = "0x";
248
- for (let i = 0; i < bytes.length; i++) {
249
- hex += bytes[i].toString(16).padStart(2, "0");
195
+
196
+ // src/statement.ts
197
+ function createExpiry(expirationTimestampSecs, sequenceNumber = 0) {
198
+ if (sequenceNumber < 0 || sequenceNumber > 4294967295) {
199
+ throw new RangeError("sequenceNumber must be 0-4294967295");
250
200
  }
251
- return hex;
201
+ return BigInt(expirationTimestampSecs) << 32n | BigInt(sequenceNumber);
252
202
  }
203
+
204
+ // src/client.ts
253
205
  var log2 = createLogger("statement-store");
254
206
  var StatementStoreClient = class {
255
207
  config;
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/types.ts","../src/errors.ts","../src/data.ts","../src/topics.ts","../src/transport.ts","../src/client.ts","../src/channels.ts"],"names":["log","createLogger","blake2b256"],"mappings":"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","file":"index.js","sourcesContent":["// Copyright 2026 Parity Technologies (UK) Ltd.\n// SPDX-License-Identifier: Apache-2.0\n// Re-export statement types from @novasamatech/sdk-statement\n\n/** A record published to the Statement Store. Every field is optional: a `data` payload, `topics` for routing, a `channel` for last-write-wins, an `expiry`, and a `proof` once signed. */\nexport type { Statement } from \"@novasamatech/sdk-statement\";\n\n/** A {@link Statement} with its `proof` attached and ready for submission — the only field the submission path actually requires. */\nexport type { SignedStatement } from \"@novasamatech/sdk-statement\";\n\n/** A {@link Statement} before signing — the same optional fields, minus `proof`. */\nexport type { UnsignedStatement } from \"@novasamatech/sdk-statement\";\n\n/** Signature attached to a statement — `sr25519`/`ed25519`/`ecdsa` plus public key, or an on-chain witness referencing a block event. */\nexport type { Proof } from \"@novasamatech/sdk-statement\";\n\n/** Outcome of a submission — new, already-known, known-expired, rejected, invalid, or internal-error. */\nexport type { SubmitResult } from \"@novasamatech/sdk-statement\";\n\n/** Upstream topic filter shape — same `\"any\"` / `matchAll` / `matchAny` structure as {@link TopicFilter}, but carries topics as 32-byte hex strings (`SizedHex<32>`) rather than the SDK's `Uint8Array`-based {@link TopicHash}. Reach for this when working with a {@link StatementTransport} directly. */\nexport type { TopicFilter as SdkTopicFilter } from \"@novasamatech/sdk-statement\";\n\nimport type {\n Statement,\n SignedStatement,\n TopicFilter as SdkTopicFilter,\n} from \"@novasamatech/sdk-statement\";\n\n// ============================================================================\n// Constants\n// ============================================================================\n\n/** Maximum size of a single statement's data payload in bytes. */\nexport const MAX_STATEMENT_SIZE = 512;\n\n/** Maximum total storage per user in bytes. */\nexport const MAX_USER_TOTAL = 1024;\n\n/** Default time-to-live for published statements in seconds. */\nexport const DEFAULT_TTL_SECONDS = 30;\n\n// ============================================================================\n// Branded Types\n// ============================================================================\n\n/**\n * A 32-byte blake2b-256 hash used as a statement topic.\n *\n * Topics allow subscribers to filter statements efficiently on the network.\n * Create one with {@link createTopic}.\n */\nexport type TopicHash = Uint8Array & { readonly __brand: \"TopicHash\" };\n\n/**\n * A 32-byte blake2b-256 hash used as a statement channel identifier.\n *\n * Channels enable last-write-wins semantics: for a given channel,\n * only the most recent statement (by timestamp) is kept.\n * Create one with {@link createChannel}.\n */\nexport type ChannelHash = Uint8Array & { readonly __brand: \"ChannelHash\" };\n\n// ============================================================================\n// Topic Filtering\n// ============================================================================\n\n/**\n * Filter for statement subscriptions and queries.\n *\n * - `\"any\"` — matches all statements (no filtering).\n * - `{ matchAll: [...] }` — matches statements that have **all** listed topics.\n * - `{ matchAny: [...] }` — matches statements that have **any** listed topic.\n */\nexport type TopicFilter = \"any\" | { matchAll: TopicHash[] } | { matchAny: TopicHash[] };\n\n// ============================================================================\n// Signer & Credentials\n// ============================================================================\n\n/**\n * A function that signs a message with an Sr25519 key.\n *\n * Takes the signature material bytes and returns a 64-byte Sr25519 signature.\n * May be synchronous or asynchronous (e.g., when signing via a hardware wallet).\n */\nexport type StatementSigner = (message: Uint8Array) => Uint8Array | Promise<Uint8Array>;\n\n/**\n * An Sr25519 signer with its associated public key.\n *\n * Used by {@link StatementStoreClient.connect} to sign published statements\n * when running outside a container (local mode).\n */\nexport interface StatementSignerWithKey {\n /** 32-byte Sr25519 public key. */\n publicKey: Uint8Array;\n /** Signing function. */\n sign: StatementSigner;\n}\n\n/**\n * Credentials for connecting to the statement store.\n *\n * - **Host mode**: Inside a container, proof creation is delegated to the host API.\n * The `accountId` is a `[ss58Address, chainPrefix]` tuple from product-sdk.\n * - **Local mode**: Outside a container, statements are signed locally using the\n * provided Sr25519 signer.\n */\nexport type ConnectionCredentials =\n | { mode: \"host\"; accountId: [string, number] }\n | { mode: \"local\"; signer: StatementSignerWithKey };\n\n// ============================================================================\n// Configuration\n// ============================================================================\n\n/**\n * Configuration for {@link StatementStoreClient}.\n *\n * The client uses the Host API's native statement store protocol.\n * The SDK is designed to run exclusively inside a host container.\n */\nexport interface StatementStoreConfig {\n /**\n * Application namespace used as the primary topic (topic1).\n *\n * All statements published by this client are tagged with `blake2b(appName)`.\n * Subscribers filter on this topic to receive only relevant statements.\n *\n * @example \"ss-webrtc\", \"mark3t-presence\", \"my-app\"\n */\n appName: string;\n\n /**\n * Default time-to-live for published statements in seconds.\n *\n * Statements automatically expire after this duration.\n * Can be overridden per-publish via {@link PublishOptions.ttlSeconds}.\n *\n * @default 30\n */\n defaultTtlSeconds?: number;\n\n /**\n * Provide a custom transport instead of auto-detection.\n *\n * When set, the client uses this transport directly.\n * Useful for testing or advanced BYOD setups.\n */\n transport?: StatementTransport;\n}\n\n// ============================================================================\n// Publish & Subscribe\n// ============================================================================\n\n/**\n * Options for publishing a single statement.\n */\nexport interface PublishOptions {\n /**\n * Channel name for last-write-wins semantics.\n *\n * When provided, the statement is tagged with `blake2b(channel)`.\n * For a given channel, only the most recent statement is kept.\n *\n * @example \"presence/peer-abc123\", \"handshake/alice-bob\"\n */\n channel?: string;\n\n /**\n * Secondary topic for additional filtering.\n *\n * Hashed with blake2b and set as topic2. Useful for scoping\n * statements to a specific room, document, or context.\n *\n * @example \"doc-abc123\", \"room-456\"\n */\n topic2?: string;\n\n /**\n * Time-to-live in seconds. Overrides {@link StatementStoreConfig.defaultTtlSeconds}.\n */\n ttlSeconds?: number;\n\n /**\n * Decryption key hint (32 bytes).\n *\n * Used by the `statement_posted` RPC method to filter statements.\n * Typically set to the blake2b hash of the room or document ID.\n */\n decryptionKey?: Uint8Array;\n}\n\n/**\n * A received statement with typed data and metadata.\n *\n * @typeParam T - The parsed data type (decoded from JSON).\n */\nexport interface ReceivedStatement<T = unknown> {\n /** Parsed data payload. */\n data: T;\n /** Signer's public key hex (from proof), if present. */\n signerHex?: string;\n /** Channel hex, if present. */\n channelHex?: string;\n /** Topics array (hex strings). */\n topics: string[];\n /** Combined expiry value (upper 32 bits = timestamp, lower 32 bits = sequence). */\n expiry?: bigint;\n /** The full statement from the transport for advanced inspection. */\n raw: Statement;\n}\n\n// ============================================================================\n// Transport\n// ============================================================================\n\n/** Handle returned by subscription methods. Call `unsubscribe()` to stop receiving events. */\nexport interface Unsubscribable {\n unsubscribe: () => void;\n}\n\n/**\n * Low-level transport interface for statement store communication.\n *\n * Uses **HostTransport** — the Host API's native binary protocol.\n *\n * Most consumers should use {@link StatementStoreClient} instead of this interface directly.\n */\nexport interface StatementTransport {\n /**\n * Subscribe to statements matching a topic filter.\n *\n * @param filter - sdk-statement topic filter.\n * @param onStatements - Called with batches of received statements.\n * @param onError - Called when the subscription encounters an error.\n * @returns A handle to unsubscribe.\n */\n subscribe(\n filter: SdkTopicFilter,\n onStatements: (statements: Statement[]) => void,\n onError: (error: Error) => void,\n ): Unsubscribable;\n\n /**\n * Sign and submit a statement.\n *\n * - **Host mode**: delegates to the host API's `createProof` then `submit`.\n * - **Local mode**: signs locally using `getStatementSigner` from sdk-statement, then submits.\n *\n * @param statement - The unsigned statement to sign and submit.\n * @param credentials - Signing credentials (host accountId or local signer).\n */\n signAndSubmit(statement: Statement, credentials: ConnectionCredentials): Promise<void>;\n\n /**\n * Query existing statements from the store.\n *\n * Note: The host API subscription replays initial state, so this may not be needed.\n */\n query?(filter: SdkTopicFilter): Promise<Statement[]>;\n\n /** Destroy the transport and release all resources. */\n destroy(): void;\n}\n","// Copyright 2026 Parity Technologies (UK) Ltd.\n// SPDX-License-Identifier: Apache-2.0\nimport { MAX_STATEMENT_SIZE } from \"./types.js\";\n\n/**\n * Base class for all statement store errors.\n *\n * Use `instanceof StatementStoreError` to catch any error originating\n * from the statement store package.\n */\nexport class StatementStoreError extends Error {\n constructor(message: string, options?: ErrorOptions) {\n super(message, options);\n this.name = \"StatementStoreError\";\n }\n}\n\n/**\n * A SCALE encoding or decoding operation failed.\n *\n * Thrown when statement bytes cannot be parsed (corrupt data, unknown field tags)\n * or when encoding produces invalid output.\n */\nexport class StatementEncodingError extends StatementStoreError {\n constructor(message: string, options?: ErrorOptions) {\n super(`Encoding error: ${message}`, options);\n this.name = \"StatementEncodingError\";\n }\n}\n\n/**\n * The statement store node rejected a submitted statement.\n *\n * Carries the raw RPC response detail for programmatic inspection.\n */\nexport class StatementSubmitError extends StatementStoreError {\n /** The raw response from the RPC call. */\n readonly detail: unknown;\n\n constructor(detail: unknown) {\n super(`Statement submission rejected: ${JSON.stringify(detail)}`);\n this.name = \"StatementSubmitError\";\n this.detail = detail;\n }\n}\n\n/**\n * Failed to set up or maintain a statement subscription.\n *\n * This is a non-fatal error — the client falls back to polling\n * when subscriptions are unavailable.\n */\nexport class StatementSubscriptionError extends StatementStoreError {\n constructor(message: string, options?: ErrorOptions) {\n super(`Subscription error: ${message}`, options);\n this.name = \"StatementSubscriptionError\";\n }\n}\n\n/**\n * Failed to connect to the statement store transport.\n *\n * Thrown when the WebSocket connection cannot be established\n * or the chain-client's bulletin chain is not connected.\n */\nexport class StatementConnectionError extends StatementStoreError {\n constructor(message: string, options?: ErrorOptions) {\n super(`Connection error: ${message}`, options);\n this.name = \"StatementConnectionError\";\n }\n}\n\n/**\n * The statement data payload exceeds the maximum allowed size.\n *\n * The statement store protocol limits individual statement data\n * to {@link MAX_STATEMENT_SIZE} bytes (512 bytes).\n */\nexport class StatementDataTooLargeError extends StatementStoreError {\n /** The actual size of the data in bytes. */\n readonly actualSize: number;\n /** The maximum allowed size in bytes. */\n readonly maxSize: number;\n\n constructor(actualSize: number, maxSize: number = MAX_STATEMENT_SIZE) {\n super(`Statement data too large: ${actualSize} bytes exceeds maximum of ${maxSize} bytes`);\n this.name = \"StatementDataTooLargeError\";\n this.actualSize = actualSize;\n this.maxSize = maxSize;\n }\n}\n\nif (import.meta.vitest) {\n const { describe, test, expect } = import.meta.vitest;\n\n describe(\"StatementStoreError hierarchy\", () => {\n test(\"StatementStoreError is instanceof Error\", () => {\n const err = new StatementStoreError(\"test\");\n expect(err).toBeInstanceOf(Error);\n expect(err).toBeInstanceOf(StatementStoreError);\n expect(err.name).toBe(\"StatementStoreError\");\n expect(err.message).toBe(\"test\");\n });\n\n test(\"StatementEncodingError\", () => {\n const err = new StatementEncodingError(\"bad field tag\");\n expect(err).toBeInstanceOf(StatementStoreError);\n expect(err).toBeInstanceOf(Error);\n expect(err.name).toBe(\"StatementEncodingError\");\n expect(err.message).toContain(\"bad field tag\");\n });\n\n test(\"StatementEncodingError preserves cause\", () => {\n const cause = new Error(\"original\");\n const err = new StatementEncodingError(\"wrap\", { cause });\n expect(err.cause).toBe(cause);\n });\n\n test(\"StatementSubmitError\", () => {\n const detail = { status: \"rejected\", reason: \"bad proof\" };\n const err = new StatementSubmitError(detail);\n expect(err).toBeInstanceOf(StatementStoreError);\n expect(err.name).toBe(\"StatementSubmitError\");\n expect(err.detail).toBe(detail);\n expect(err.message).toContain(\"rejected\");\n });\n\n test(\"StatementSubscriptionError\", () => {\n const err = new StatementSubscriptionError(\"not supported\");\n expect(err).toBeInstanceOf(StatementStoreError);\n expect(err.name).toBe(\"StatementSubscriptionError\");\n expect(err.message).toContain(\"not supported\");\n });\n\n test(\"StatementConnectionError\", () => {\n const err = new StatementConnectionError(\"timeout\");\n expect(err).toBeInstanceOf(StatementStoreError);\n expect(err.name).toBe(\"StatementConnectionError\");\n expect(err.message).toContain(\"timeout\");\n });\n\n test(\"StatementDataTooLargeError\", () => {\n const err = new StatementDataTooLargeError(600);\n expect(err).toBeInstanceOf(StatementStoreError);\n expect(err.name).toBe(\"StatementDataTooLargeError\");\n expect(err.actualSize).toBe(600);\n expect(err.maxSize).toBe(512);\n expect(err.message).toContain(\"600\");\n expect(err.message).toContain(\"512\");\n });\n\n test(\"StatementDataTooLargeError with custom max\", () => {\n const err = new StatementDataTooLargeError(2000, 1024);\n expect(err.actualSize).toBe(2000);\n expect(err.maxSize).toBe(1024);\n });\n\n test(\"all errors are catchable via base class\", () => {\n const errors = [\n new StatementEncodingError(\"test\"),\n new StatementSubmitError(\"test\"),\n new StatementSubscriptionError(\"test\"),\n new StatementConnectionError(\"test\"),\n new StatementDataTooLargeError(600),\n ];\n for (const err of errors) {\n expect(err).toBeInstanceOf(StatementStoreError);\n }\n });\n });\n}\n","// Copyright 2026 Parity Technologies (UK) Ltd.\n// SPDX-License-Identifier: Apache-2.0\nimport { StatementDataTooLargeError, StatementEncodingError } from \"./errors.js\";\nimport { MAX_STATEMENT_SIZE } from \"./types.js\";\n\n/** Convert a hex string (with or without 0x prefix) to bytes. */\nexport function fromHex(hex: string): Uint8Array {\n const clean = hex.startsWith(\"0x\") ? hex.slice(2) : hex;\n const bytes = new Uint8Array(clean.length / 2);\n for (let i = 0; i < bytes.length; i++) {\n bytes[i] = Number.parseInt(clean.substring(i * 2, i * 2 + 2), 16);\n }\n return bytes;\n}\n\n/** Convert bytes to a hex string with 0x prefix. */\nexport function toHex(bytes: Uint8Array): string {\n let hex = \"0x\";\n for (let i = 0; i < bytes.length; i++) {\n hex += bytes[i].toString(16).padStart(2, \"0\");\n }\n return hex;\n}\n\n/**\n * Encode a value as a JSON-serialized data payload for a statement.\n *\n * Serializes the value as JSON and encodes to UTF-8 bytes.\n * Throws {@link StatementDataTooLargeError} if the result exceeds\n * {@link MAX_STATEMENT_SIZE} bytes.\n *\n * @typeParam T - The type of value being encoded.\n * @param value - The value to serialize.\n * @returns UTF-8 encoded JSON bytes.\n * @throws {StatementDataTooLargeError} If the encoded data exceeds 512 bytes.\n */\nexport function encodeData<T>(value: T): Uint8Array {\n const json = JSON.stringify(value);\n const bytes = new TextEncoder().encode(json);\n if (bytes.length > MAX_STATEMENT_SIZE) {\n throw new StatementDataTooLargeError(bytes.length);\n }\n return bytes;\n}\n\n/**\n * Decode a JSON-serialized data payload from statement bytes.\n *\n * @typeParam T - The expected parsed type.\n * @param bytes - UTF-8 encoded JSON bytes.\n * @returns The parsed value.\n * @throws {StatementEncodingError} If the bytes are not valid UTF-8 or valid JSON.\n */\nexport function decodeData<T>(bytes: Uint8Array): T {\n try {\n const json = new TextDecoder(\"utf-8\", { fatal: true }).decode(bytes);\n return JSON.parse(json) as T;\n } catch (error) {\n throw new StatementEncodingError(\"Failed to decode JSON data\", { cause: error });\n }\n}\n\nif (import.meta.vitest) {\n const { describe, test, expect } = import.meta.vitest;\n\n describe(\"encodeData / decodeData\", () => {\n test(\"round-trips JSON object\", () => {\n const original = { type: \"presence\", peerId: \"abc123\", timestamp: 1234 };\n const encoded = encodeData(original);\n const decoded = decodeData<typeof original>(encoded);\n expect(decoded).toEqual(original);\n });\n\n test(\"round-trips string\", () => {\n const encoded = encodeData(\"hello\");\n expect(decodeData<string>(encoded)).toBe(\"hello\");\n });\n\n test(\"round-trips number\", () => {\n const encoded = encodeData(42);\n expect(decodeData<number>(encoded)).toBe(42);\n });\n\n test(\"throws StatementDataTooLargeError for oversized data\", () => {\n const large = { data: \"x\".repeat(600) };\n expect(() => encodeData(large)).toThrow(StatementDataTooLargeError);\n });\n\n test(\"allows data at exactly MAX_STATEMENT_SIZE\", () => {\n const str = \"a\".repeat(510);\n const encoded = encodeData(str);\n expect(encoded.length).toBe(512);\n });\n\n test(\"throws StatementEncodingError for invalid JSON bytes\", () => {\n const invalid = new TextEncoder().encode(\"{not valid json\");\n expect(() => decodeData(invalid)).toThrow(StatementEncodingError);\n });\n\n test(\"throws StatementEncodingError for non-UTF-8 bytes\", () => {\n const invalid = new Uint8Array([0xff, 0xfe, 0xfd]);\n expect(() => decodeData(invalid)).toThrow(StatementEncodingError);\n });\n });\n\n describe(\"toHex / fromHex\", () => {\n test(\"round-trips bytes\", () => {\n const bytes = new Uint8Array([0xde, 0xad, 0xbe, 0xef]);\n const hex = toHex(bytes);\n expect(hex).toBe(\"0xdeadbeef\");\n expect(fromHex(hex)).toEqual(bytes);\n });\n\n test(\"handles hex without 0x prefix\", () => {\n const bytes = fromHex(\"cafebabe\");\n expect(bytes).toEqual(new Uint8Array([0xca, 0xfe, 0xba, 0xbe]));\n });\n\n test(\"handles empty input\", () => {\n expect(toHex(new Uint8Array(0))).toBe(\"0x\");\n expect(fromHex(\"0x\")).toEqual(new Uint8Array(0));\n });\n });\n}\n","// Copyright 2026 Parity Technologies (UK) Ltd.\n// SPDX-License-Identifier: Apache-2.0\nimport { blake2b256 } from \"@parity/product-sdk-utils\";\n\nimport type { ChannelHash, TopicFilter, TopicHash, SdkTopicFilter } from \"./types.js\";\n\n/**\n * Create a 32-byte topic hash from a human-readable string.\n *\n * Uses blake2b-256 to hash the UTF-8 encoded string into a deterministic\n * 32-byte topic identifier. Statements are tagged with topics so subscribers\n * can filter efficiently on the network.\n *\n * @param name - A human-readable topic name (e.g., \"ss-webrtc\", \"my-app\").\n * @returns A branded 32-byte topic hash.\n *\n * @example\n * ```ts\n * const topic = createTopic(\"ss-webrtc\");\n * // Use in subscriptions and publish options\n * ```\n */\nexport function createTopic(name: string): TopicHash {\n const bytes = new TextEncoder().encode(name);\n return blake2b256(bytes) as TopicHash;\n}\n\n/**\n * Create a 32-byte channel hash from a human-readable channel name.\n *\n * Channels enable last-write-wins semantics: for a given channel,\n * only the most recent statement (by timestamp) is retained.\n *\n * @param name - A human-readable channel name (e.g., \"presence/peer-abc\").\n * @returns A branded 32-byte channel hash.\n *\n * @example\n * ```ts\n * const channel = createChannel(\"presence/peer-abc123\");\n * ```\n */\nexport function createChannel(name: string): ChannelHash {\n const bytes = new TextEncoder().encode(name);\n return blake2b256(bytes) as ChannelHash;\n}\n\n/**\n * Convert a topic or channel hash to a hex string (with 0x prefix).\n *\n * @param hash - A 32-byte topic or channel hash.\n * @returns Hex string with \"0x\" prefix.\n */\nexport function topicToHex(hash: Uint8Array): string {\n let hex = \"0x\";\n for (let i = 0; i < hash.length; i++) {\n hex += hash[i].toString(16).padStart(2, \"0\");\n }\n return hex;\n}\n\n/**\n * Compare two topic or channel hashes for byte equality.\n *\n * @param a - First hash.\n * @param b - Second hash.\n * @returns True if the hashes are identical.\n */\nexport function topicsEqual(a: Uint8Array, b: Uint8Array): boolean {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (a[i] !== b[i]) return false;\n }\n return true;\n}\n\n/**\n * Serialize a {@link TopicFilter} into the JSON-RPC format expected by statement store nodes.\n *\n * - `\"any\"` is passed through as the string `\"any\"`.\n * - `{ matchAll: [...] }` becomes `{ matchAll: [\"0x...\", ...] }` with hex-encoded topics.\n * - `{ matchAny: [...] }` becomes `{ matchAny: [\"0x...\", ...] }` with hex-encoded topics.\n *\n * @param filter - The topic filter to serialize.\n * @returns A JSON-RPC compatible filter value.\n */\nexport function serializeTopicFilter(filter: TopicFilter): SdkTopicFilter {\n if (filter === \"any\") return \"any\";\n\n if (\"matchAll\" in filter) {\n return { matchAll: filter.matchAll.map(topicToHex) };\n }\n\n return { matchAny: filter.matchAny.map(topicToHex) };\n}\n\nif (import.meta.vitest) {\n const { describe, test, expect } = import.meta.vitest;\n\n describe(\"createTopic\", () => {\n test(\"produces a 32-byte hash\", () => {\n const topic = createTopic(\"test\");\n expect(topic).toBeInstanceOf(Uint8Array);\n expect(topic.length).toBe(32);\n });\n\n test(\"is deterministic (same input = same hash)\", () => {\n const a = createTopic(\"ss-webrtc\");\n const b = createTopic(\"ss-webrtc\");\n expect(topicsEqual(a, b)).toBe(true);\n });\n\n test(\"different inputs produce different hashes\", () => {\n const a = createTopic(\"topic-a\");\n const b = createTopic(\"topic-b\");\n expect(topicsEqual(a, b)).toBe(false);\n });\n\n test(\"empty string produces a valid hash\", () => {\n const topic = createTopic(\"\");\n expect(topic.length).toBe(32);\n });\n });\n\n describe(\"createChannel\", () => {\n test(\"produces a 32-byte hash\", () => {\n const channel = createChannel(\"presence/peer-abc\");\n expect(channel).toBeInstanceOf(Uint8Array);\n expect(channel.length).toBe(32);\n });\n\n test(\"same input as createTopic produces same bytes\", () => {\n const topic = createTopic(\"test-name\");\n const channel = createChannel(\"test-name\");\n expect(topicsEqual(topic, channel)).toBe(true);\n });\n });\n\n describe(\"topicToHex\", () => {\n test(\"converts to hex with 0x prefix\", () => {\n const hash = new Uint8Array(32);\n hash[0] = 0xab;\n hash[1] = 0xcd;\n const hex = topicToHex(hash);\n expect(hex).toMatch(/^0x/);\n expect(hex).toBe(`0xabcd${\"00\".repeat(30)}`);\n });\n\n test(\"round-trips through hex encoding\", () => {\n const topic = createTopic(\"round-trip-test\");\n const hex = topicToHex(topic);\n expect(hex.length).toBe(2 + 64); // \"0x\" + 64 hex chars\n });\n\n test(\"pads single-digit bytes with leading zero\", () => {\n const hash = new Uint8Array([0x0a]);\n expect(topicToHex(hash)).toBe(\"0x0a\");\n });\n });\n\n describe(\"topicsEqual\", () => {\n test(\"returns true for identical hashes\", () => {\n const a = createTopic(\"same\");\n const b = createTopic(\"same\");\n expect(topicsEqual(a, b)).toBe(true);\n });\n\n test(\"returns false for different hashes\", () => {\n const a = createTopic(\"alpha\");\n const b = createTopic(\"beta\");\n expect(topicsEqual(a, b)).toBe(false);\n });\n\n test(\"returns false for different lengths\", () => {\n const a = new Uint8Array(32);\n const b = new Uint8Array(16);\n expect(topicsEqual(a, b)).toBe(false);\n });\n\n test(\"returns true for empty arrays\", () => {\n expect(topicsEqual(new Uint8Array(0), new Uint8Array(0))).toBe(true);\n });\n });\n\n describe(\"serializeTopicFilter\", () => {\n test(\"serializes 'any' as string\", () => {\n expect(serializeTopicFilter(\"any\")).toBe(\"any\");\n });\n\n test(\"serializes matchAll with hex topics\", () => {\n const topics = [createTopic(\"a\"), createTopic(\"b\")];\n const result = serializeTopicFilter({ matchAll: topics }) as {\n matchAll: string[];\n };\n expect(result.matchAll).toHaveLength(2);\n expect(result.matchAll[0]).toMatch(/^0x[0-9a-f]{64}$/);\n expect(result.matchAll[1]).toMatch(/^0x[0-9a-f]{64}$/);\n });\n\n test(\"serializes matchAny with hex topics\", () => {\n const topics = [createTopic(\"x\")];\n const result = serializeTopicFilter({ matchAny: topics }) as {\n matchAny: string[];\n };\n expect(result.matchAny).toHaveLength(1);\n expect(result.matchAny[0]).toMatch(/^0x[0-9a-f]{64}$/);\n });\n\n test(\"matchAll preserves order\", () => {\n const topicA = createTopic(\"first\");\n const topicB = createTopic(\"second\");\n const result = serializeTopicFilter({ matchAll: [topicA, topicB] }) as {\n matchAll: string[];\n };\n expect(result.matchAll[0]).toBe(topicToHex(topicA));\n expect(result.matchAll[1]).toBe(topicToHex(topicB));\n });\n });\n}\n","// Copyright 2026 Parity Technologies (UK) Ltd.\n// SPDX-License-Identifier: Apache-2.0\nimport { createLogger } from \"@parity/product-sdk-logger\";\nimport type { HostStatementStore, StatementTopicFilter } from \"@parity/product-sdk-host\";\n\nimport { StatementConnectionError, StatementSubscriptionError } from \"./errors.js\";\nimport type { ConnectionCredentials, StatementTransport, Unsubscribable } from \"./types.js\";\n\nimport type { Statement, TopicFilter as SdkTopicFilter } from \"@novasamatech/sdk-statement\";\n\n// Type-only imports for the host↔sdk bridge (erased at compile time, zero bundle cost).\n// product-sdk's SCALE-decoded types use Uint8Array + { tag } enums;\n// sdk-statement's types use hex strings + { type } enums.\nimport type {\n Statement as HostStatement,\n SignedStatement as HostSignedStatement,\n} from \"@novasamatech/host-api-wrapper\";\n\nconst log = createLogger(\"statement-store:transport\");\n\n// ============================================================================\n// Host Transport — uses the Host API's native binary protocol\n// ============================================================================\n\n/**\n * Statement transport that uses the Host API inside containers.\n *\n * Communicates through the host's native `remote_statement_store_*` protocol\n * which bypasses JSON-RPC entirely. Subscriptions, proof creation, and submission\n * all go through typed binary messages over the host transport.\n */\nclass HostTransport implements StatementTransport {\n private readonly store: HostStatementStore;\n\n constructor(store: HostStatementStore) {\n this.store = store;\n }\n\n subscribe(\n filter: SdkTopicFilter,\n onStatements: (statements: Statement[]) => void,\n onError: (error: Error) => void,\n ): Unsubscribable {\n const hostFilter = sdkFilterToHost(filter);\n\n try {\n const sub = this.store.subscribe(hostFilter, (page) => {\n // product-sdk delivers HostSignedStatement[] (Uint8Array fields, { tag } enums)\n // inside a StatementsPage. sdk-statement expects Statement (hex string fields,\n // { type } enums). Type assertion needed: page.statements is unknown[] at the\n // interface boundary but actual runtime values are HostSignedStatement[].\n const converted = (page.statements as HostSignedStatement[]).map(\n hostSignedStatementToSdk,\n );\n onStatements(converted);\n });\n\n log.info(\"Host subscription active\");\n\n return {\n unsubscribe: () => sub.unsubscribe(),\n };\n } catch (error) {\n const msg = error instanceof Error ? error.message : String(error);\n log.warn(\"Host subscription failed\", { error: msg });\n onError(new StatementSubscriptionError(msg));\n return { unsubscribe: () => {} };\n }\n }\n\n async signAndSubmit(statement: Statement, credentials: ConnectionCredentials): Promise<void> {\n if (credentials.mode !== \"host\") {\n throw new StatementConnectionError(\n \"HostTransport requires host credentials. Use { mode: 'host', accountId } to connect.\",\n );\n }\n\n // Convert sdk-statement format (hex strings) → product-sdk format (Uint8Array)\n // so the host's SCALE codec can encode it correctly.\n const hostStatement = sdkStatementToHost(statement);\n const proof = await this.store.createProof(credentials.accountId, hostStatement);\n // Type assertion: StatementProof from host types is compatible with HostSignedStatement.proof\n // at runtime, but TypeScript can't verify cross-package type compatibility.\n const signedStatement: HostSignedStatement = {\n ...hostStatement,\n proof: proof as HostSignedStatement[\"proof\"],\n };\n await this.store.submit(signedStatement);\n\n log.debug(\"Statement submitted via host\");\n }\n\n destroy(): void {\n // Host owns the transport — nothing to clean up\n }\n}\n\n// ============================================================================\n// Transport Factory\n// ============================================================================\n\n/**\n * Create a statement store transport.\n *\n * Uses the Host API via `@parity/product-sdk-host` — the container's native\n * statement store protocol (binary, not JSON-RPC). This is the only supported path.\n *\n * @throws {StatementConnectionError} If the host statement store is unavailable.\n */\nexport async function createTransport(): Promise<StatementTransport> {\n const { getStatementStore } = await import(\"@parity/product-sdk-host\");\n const store = await getStatementStore();\n\n if (!store) {\n throw new StatementConnectionError(\n \"Host statement store unavailable. Ensure you are running inside a host container (Polkadot Browser / Desktop).\",\n );\n }\n\n log.info(\"Using host API statement store transport\");\n return new HostTransport(store);\n}\n\n// ============================================================================\n// Internal Helpers\n// ============================================================================\n\n// ============================================================================\n// Host ↔ SDK Type Bridge\n//\n// product-sdk types (SCALE-decoded): Uint8Array fields, { tag: \"Sr25519\" } enums\n// sdk-statement types: hex string fields, { type: \"sr25519\" } enums\n//\n// Both represent the same on-chain statement data but with different runtime\n// shapes. These converters bridge between the two so HostTransport can speak\n// both languages correctly.\n// ============================================================================\n\n/** Convert an sdk-statement TopicFilter (hex strings) → host StatementTopicFilter (Uint8Array). */\nfunction sdkFilterToHost(filter: SdkTopicFilter): StatementTopicFilter {\n if (filter === \"any\") {\n // The host API has no \"match anything\" variant — express it as\n // \"match any of zero topics\" which the host treats as a wildcard.\n return { matchAny: [] };\n }\n if (\"matchAll\" in filter) {\n return { matchAll: filter.matchAll.map(hexToBytes) };\n }\n return { matchAny: filter.matchAny.map(hexToBytes) };\n}\n\n/** Convert a product-sdk SignedStatement (Uint8Array fields) → sdk-statement Statement (hex strings). */\nfunction hostSignedStatementToSdk(hostStmt: HostSignedStatement): Statement {\n const result: Partial<Statement> = {};\n\n // data: Uint8Array → Uint8Array (same in both formats)\n if (hostStmt.data) result.data = hostStmt.data;\n\n // expiry: bigint → bigint (same in both formats)\n if (hostStmt.expiry !== undefined) result.expiry = hostStmt.expiry;\n\n // topics: Uint8Array[] → hex string[]\n if (hostStmt.topics) {\n result.topics = hostStmt.topics.map(bytesToHex) as Statement[\"topics\"];\n }\n\n // channel: Uint8Array → hex string\n if (hostStmt.channel) {\n result.channel = bytesToHex(hostStmt.channel) as Statement[\"channel\"];\n }\n\n // decryptionKey: Uint8Array → hex string\n if (hostStmt.decryptionKey) {\n result.decryptionKey = bytesToHex(hostStmt.decryptionKey) as Statement[\"decryptionKey\"];\n }\n\n // proof: { tag: \"Sr25519\" | \"Ed25519\" | \"Ecdsa\" | \"OnChain\", value: ... }\n // → { type: \"sr25519\" | \"ed25519\" | \"ecdsa\" | \"onChain\", value: ... } (with hex)\n if (hostStmt.proof) {\n const tag = hostStmt.proof.tag;\n const value = hostStmt.proof.value;\n // Product-sdk proof tags use PascalCase; sdk-statement variants are camelCase.\n const sdkType =\n tag === \"OnChain\" ? \"onChain\" : (tag.toLowerCase() as \"sr25519\" | \"ed25519\" | \"ecdsa\");\n\n if (sdkType === \"onChain\" && \"who\" in value) {\n result.proof = {\n type: \"onChain\",\n value: {\n who: bytesToHex(value.who),\n blockHash: bytesToHex(value.blockHash),\n event: value.event,\n },\n } as Statement[\"proof\"];\n } else if (\"signature\" in value && \"signer\" in value) {\n result.proof = {\n type: sdkType,\n value: {\n signature: bytesToHex(value.signature),\n signer: bytesToHex(value.signer),\n },\n } as Statement[\"proof\"];\n }\n }\n\n return result as Statement;\n}\n\n/** Convert an sdk-statement Statement (hex strings) → product-sdk HostStatement (Uint8Array). */\nfunction sdkStatementToHost(stmt: Statement): HostStatement {\n const result: Partial<HostStatement> = {};\n\n // data: Uint8Array → Uint8Array (same)\n if (stmt.data) result.data = stmt.data;\n\n // expiry: bigint → bigint (same)\n if (stmt.expiry !== undefined) result.expiry = stmt.expiry;\n\n // topics: hex string[] → Uint8Array[]\n if (stmt.topics) {\n result.topics = stmt.topics.map(hexToBytes);\n }\n\n // channel: hex string → Uint8Array\n if (stmt.channel) {\n result.channel = hexToBytes(stmt.channel);\n }\n\n // decryptionKey: hex string → Uint8Array\n if (stmt.decryptionKey) {\n result.decryptionKey = hexToBytes(stmt.decryptionKey);\n }\n\n return result as HostStatement;\n}\n\n/** Convert a 0x-prefixed hex string to Uint8Array. */\nfunction hexToBytes(hex: string): Uint8Array {\n const clean = hex.startsWith(\"0x\") ? hex.slice(2) : hex;\n const bytes = new Uint8Array(clean.length / 2);\n for (let i = 0; i < bytes.length; i++) {\n bytes[i] = Number.parseInt(clean.substring(i * 2, i * 2 + 2), 16);\n }\n return bytes;\n}\n\n/** Convert Uint8Array to 0x-prefixed hex string. */\nfunction bytesToHex(bytes: Uint8Array): string {\n let hex = \"0x\";\n for (let i = 0; i < bytes.length; i++) {\n hex += bytes[i].toString(16).padStart(2, \"0\");\n }\n return hex;\n}\n","// Copyright 2026 Parity Technologies (UK) Ltd.\n// SPDX-License-Identifier: Apache-2.0\nimport { createLogger } from \"@parity/product-sdk-logger\";\nimport { blake2b256 } from \"@parity/product-sdk-utils\";\n\nimport { decodeData, encodeData, toHex } from \"./data.js\";\nimport { StatementConnectionError } from \"./errors.js\";\nimport { createChannel, createTopic, serializeTopicFilter, topicToHex } from \"./topics.js\";\nimport { createTransport } from \"./transport.js\";\nimport type {\n ConnectionCredentials,\n PublishOptions,\n ReceivedStatement,\n StatementSignerWithKey,\n StatementStoreConfig,\n StatementTransport,\n Unsubscribable,\n} from \"./types.js\";\nimport { DEFAULT_TTL_SECONDS } from \"./types.js\";\n\nimport type { Statement } from \"@novasamatech/sdk-statement\";\nimport { createExpiry } from \"@novasamatech/sdk-statement\";\nimport type { SdkTopicFilter } from \"./types.js\";\n\nconst log = createLogger(\"statement-store\");\n\n/**\n * High-level client for the Polkadot Statement Store.\n *\n * Provides a simple publish/subscribe API over the ephemeral statement store,\n * handling topic management and signing through the host API.\n *\n * The SDK is designed to run exclusively inside a host container.\n *\n * @example\n * ```ts\n * import { StatementStoreClient } from \"@parity/product-sdk-statement-store\";\n *\n * // Inside a container (host mode)\n * const client = new StatementStoreClient({ appName: \"my-app\" });\n * await client.connect({ mode: \"host\", accountId: [\"5Grw...\", 42] });\n *\n * // Publish\n * await client.publish({ type: \"presence\", peerId: \"abc\" }, {\n * channel: \"presence/abc\",\n * topic2: \"room-123\",\n * });\n *\n * // Subscribe\n * client.subscribe<{ type: string }>(statement => {\n * console.log(statement.data.type);\n * });\n *\n * // Cleanup\n * client.destroy();\n * ```\n */\nexport class StatementStoreClient {\n private readonly config: Required<Pick<StatementStoreConfig, \"appName\" | \"defaultTtlSeconds\">> &\n Pick<StatementStoreConfig, \"transport\">;\n\n private transport: StatementTransport | null = null;\n private credentials: ConnectionCredentials | null = null;\n private subscription: Unsubscribable | null = null;\n private callbacks: Array<(statement: ReceivedStatement<unknown>) => void> = [];\n private connected = false;\n private connectPromise: Promise<void> | null = null;\n /** Set by destroy() so doConnect() can abort cleanly if destroy races with an in-flight connect. */\n private destroyed = false;\n\n /**\n * Track seen statements by channel hex to avoid re-delivering the same statement.\n * Maps channel hex (or data hash) to the expiry value.\n */\n private seen = new Map<string, bigint>();\n\n /** Monotonic counter to ensure unique sequence numbers even within the same millisecond. */\n private sequenceCounter = 0;\n\n /** Cached hex topic string for the app name, used as the primary subscription topic. */\n private readonly appTopicHex: string;\n\n constructor(config: StatementStoreConfig) {\n this.config = {\n appName: config.appName,\n defaultTtlSeconds: config.defaultTtlSeconds ?? DEFAULT_TTL_SECONDS,\n transport: config.transport,\n };\n this.appTopicHex = topicToHex(createTopic(config.appName));\n }\n\n /**\n * Connect to the statement store and start receiving statements.\n *\n * @param credentials - Connection credentials (host accountId or local signer).\n * @throws {StatementConnectionError} If the transport cannot be established.\n */\n async connect(credentials: ConnectionCredentials): Promise<void>;\n /** @deprecated Use `connect({ mode: \"local\", signer })` instead. */\n async connect(signer: StatementSignerWithKey): Promise<void>;\n async connect(arg: ConnectionCredentials | StatementSignerWithKey): Promise<void> {\n if (this.destroyed) {\n throw new StatementConnectionError(\n \"Cannot connect: client has been destroyed. Create a new instance.\",\n );\n }\n if (this.connected) {\n log.warn(\"Already connected, ignoring duplicate connect()\");\n return;\n }\n if (this.connectPromise) {\n return this.connectPromise;\n }\n\n const credentials: ConnectionCredentials =\n \"mode\" in arg ? arg : { mode: \"local\", signer: arg };\n\n this.connectPromise = this.doConnect(credentials).finally(() => {\n this.connectPromise = null;\n });\n return this.connectPromise;\n }\n\n /* @integration */\n private async doConnect(credentials: ConnectionCredentials): Promise<void> {\n this.credentials = credentials;\n const transport = this.config.transport ?? (await createTransport());\n\n // destroy() may have been called while we were awaiting createTransport().\n // If so, clean up the newly-created transport (if we own it) instead of leaking.\n if (this.destroyed) {\n if (transport !== this.config.transport) {\n transport.destroy();\n }\n return;\n }\n\n this.transport = transport;\n\n try {\n log.info(\"Connected\", { appName: this.config.appName });\n\n this.startSubscription();\n this.connected = true;\n } catch (error) {\n this.destroy();\n throw error;\n }\n }\n\n /**\n * Publish typed data to the statement store.\n *\n * @typeParam T - The type of data being published.\n * @param data - The value to publish (must be JSON-serializable, max 512 bytes).\n * @param options - Optional channel, topic2, TTL, and decryption key overrides.\n * @returns `true` if accepted, `false` if rejected or errored.\n * @throws {StatementConnectionError} If not connected.\n * @throws {StatementDataTooLargeError} If the encoded data exceeds 512 bytes.\n */\n async publish<T>(data: T, options?: PublishOptions): Promise<boolean> {\n if (!this.transport || !this.credentials) {\n throw new StatementConnectionError(\"Not connected. Call connect() first.\");\n }\n\n const dataBytes = encodeData(data);\n const ttl = options?.ttlSeconds ?? this.config.defaultTtlSeconds;\n const expirationTimestamp = Math.floor(Date.now() / 1000) + ttl;\n const sequenceNumber = (Date.now() + this.sequenceCounter++) % 0xffffffff;\n const expiry = createExpiry(expirationTimestamp, sequenceNumber);\n\n const topics: `0x${string}`[] = [this.appTopicHex as `0x${string}`];\n if (options?.topic2) {\n topics.push(topicToHex(createTopic(options.topic2)) as `0x${string}`);\n }\n\n const statement: Statement = {\n expiry,\n topics,\n channel: options?.channel\n ? (topicToHex(createChannel(options.channel)) as `0x${string}`)\n : undefined,\n decryptionKey: options?.decryptionKey\n ? (topicToHex(options.decryptionKey) as `0x${string}`)\n : undefined,\n data: dataBytes,\n };\n\n try {\n await this.transport.signAndSubmit(statement, this.credentials);\n log.debug(\"Published\", { channel: options?.channel });\n return true;\n } catch (error) {\n log.error(\"Publish failed\", {\n error: error instanceof Error ? error.message : String(error),\n });\n return false;\n }\n }\n\n /**\n * Subscribe to incoming statements on this application's topic.\n *\n * @typeParam T - The expected data type (decoded from JSON).\n * @param callback - Called for each new statement.\n * @param options - Optional secondary topic filter.\n * @returns A handle to unsubscribe.\n */\n subscribe<T>(\n callback: (statement: ReceivedStatement<T>) => void,\n options?: { topic2?: string },\n ): Unsubscribable {\n const topic2Hex = options?.topic2 ? topicToHex(createTopic(options.topic2)) : undefined;\n\n const wrappedCallback = (statement: ReceivedStatement<unknown>) => {\n if (topic2Hex) {\n if (!statement.topics[1] || statement.topics[1] !== topic2Hex) return;\n }\n callback(statement as ReceivedStatement<T>);\n };\n\n this.callbacks.push(wrappedCallback);\n\n return {\n unsubscribe: () => {\n const index = this.callbacks.indexOf(wrappedCallback);\n if (index >= 0) {\n this.callbacks.splice(index, 1);\n }\n },\n };\n }\n\n /** Whether the client is connected and ready to publish/subscribe. */\n isConnected(): boolean {\n return this.connected;\n }\n\n /**\n * Get the signer's public key as a hex string (with 0x prefix).\n *\n * @returns The hex-encoded public key, or empty string if not connected or in host mode.\n */\n getPublicKeyHex(): string {\n if (this.credentials?.mode === \"local\") {\n return topicToHex(this.credentials.signer.publicKey);\n }\n return \"\";\n }\n\n /**\n * Destroy the client, unsubscribing and closing the transport.\n *\n * Safe to call multiple times. After destruction, the client cannot be reused.\n */\n destroy(): void {\n // Signal to any in-flight doConnect() that cleanup should happen on its side.\n this.destroyed = true;\n\n if (this.subscription) {\n this.subscription.unsubscribe();\n this.subscription = null;\n }\n\n if (this.transport) {\n this.transport.destroy();\n this.transport = null;\n }\n\n this.credentials = null;\n this.connected = false;\n this.connectPromise = null;\n this.callbacks = [];\n this.seen.clear();\n\n log.info(\"Destroyed\");\n }\n\n // ========================================================================\n // Internal\n // ========================================================================\n\n /* @integration */\n private startSubscription(): void {\n if (!this.transport) return;\n\n const filter = this.buildFilter();\n\n this.subscription = this.transport.subscribe(\n filter,\n (statements) => {\n for (const stmt of statements) {\n this.handleStatementReceived(stmt);\n }\n },\n (error) => {\n log.warn(\"Subscription error\", {\n error: error.message,\n });\n },\n );\n }\n\n /** Remove entries from the seen map whose expiry timestamp is in the past. */\n private pruneSeenMap(): void {\n const nowSeconds = BigInt(Math.floor(Date.now() / 1000));\n for (const [key, expiry] of this.seen) {\n const expiryTimestamp = expiry >> 32n;\n if (expiryTimestamp > 0n && expiryTimestamp < nowSeconds) {\n this.seen.delete(key);\n }\n }\n }\n\n /**\n * Process a received statement, dedup, parse, and deliver to callbacks.\n * Returns true if the statement was new and delivered.\n */\n private handleStatementReceived(stmt: Statement): boolean {\n this.pruneSeenMap();\n\n const parsed = this.parseStatement<unknown>(stmt);\n if (!parsed) return false;\n\n // Deduplication key: channel hex (if present) or blake2b hash of data\n const dedupeKey =\n parsed.channelHex ?? (parsed.raw.data ? toHex(blake2b256(parsed.raw.data)) : \"\");\n\n const existingExpiry = this.seen.get(dedupeKey);\n const newExpiry = parsed.expiry ?? 0n;\n\n if (existingExpiry !== undefined && newExpiry <= existingExpiry) {\n return false;\n }\n\n this.seen.set(dedupeKey, newExpiry);\n\n for (const callback of [...this.callbacks]) {\n try {\n callback(parsed);\n } catch (error) {\n log.error(\"Callback error\", {\n error: error instanceof Error ? error.message : String(error),\n });\n }\n }\n\n return true;\n }\n\n private parseStatement<T>(stmt: Statement): ReceivedStatement<T> | null {\n try {\n if (!stmt.data) return null;\n\n const data = decodeData<T>(stmt.data);\n\n // Extract signer from proof if present\n let signerHex: string | undefined;\n if (stmt.proof) {\n const proofValue = stmt.proof.value as Record<string, unknown>;\n if (\"signer\" in proofValue && typeof proofValue.signer === \"string\") {\n signerHex = proofValue.signer;\n }\n }\n\n return {\n data,\n signerHex,\n channelHex: stmt.channel,\n topics: stmt.topics ?? [],\n expiry: stmt.expiry,\n raw: stmt,\n };\n } catch {\n return null;\n }\n }\n\n /** Build an SdkTopicFilter for the app's primary topic. */\n private buildFilter(topic2Name?: string): SdkTopicFilter {\n const topics = [createTopic(this.config.appName)];\n if (topic2Name) topics.push(createTopic(topic2Name));\n return serializeTopicFilter({ matchAll: topics });\n }\n}\n","// Copyright 2026 Parity Technologies (UK) Ltd.\n// SPDX-License-Identifier: Apache-2.0\nimport { createLogger } from \"@parity/product-sdk-logger\";\n\nimport type { StatementStoreClient } from \"./client.js\";\nimport { createChannel, topicToHex } from \"./topics.js\";\nimport type { PublishOptions, ReceivedStatement, Unsubscribable } from \"./types.js\";\n\nconst log = createLogger(\"statement-store:channels\");\n\n/**\n * Higher-level abstraction providing last-write-wins channel semantics\n * over the statement store.\n *\n * Each channel name maps to a single value. When a new value is written\n * to a channel, it replaces the previous one if its timestamp is newer.\n * This is ideal for presence announcements, signaling, and ephemeral state.\n *\n * @typeParam T - The type of values stored in channels.\n * Values should include a `timestamp` field for ordering;\n * if omitted, the current time is used automatically.\n *\n * @example\n * ```ts\n * interface Presence {\n * type: \"presence\";\n * peerId: string;\n * timestamp: number;\n * }\n *\n * const channels = new ChannelStore<Presence>(client, { topic2: \"doc-123\" });\n *\n * // Write presence\n * await channels.write(\"presence/peer-abc\", {\n * type: \"presence\",\n * peerId: \"abc\",\n * timestamp: Date.now(),\n * });\n *\n * // Read all presences\n * for (const [name, value] of channels.readAll()) {\n * console.log(`${name}: ${value.peerId}`);\n * }\n *\n * // React to changes\n * channels.onChange((name, value, previous) => {\n * console.log(`Channel ${name} updated`);\n * });\n *\n * channels.destroy();\n * ```\n */\nexport class ChannelStore<T extends { timestamp?: number }> {\n private readonly client: StatementStoreClient;\n private readonly topic2: string | undefined;\n private readonly values = new Map<string, T>();\n /** Maps human-readable channel names to their hex hash keys, for consistent lookup. */\n private readonly nameToHash = new Map<string, string>();\n private readonly changeCallbacks: Array<\n (channelName: string, value: T, previous: T | undefined) => void\n > = [];\n private subscription: Unsubscribable | null = null;\n\n /**\n * @param client - The connected {@link StatementStoreClient} to use.\n * @param options - Optional secondary topic for scoping channels.\n */\n constructor(client: StatementStoreClient, options?: { topic2?: string }) {\n this.client = client;\n this.topic2 = options?.topic2;\n\n // Subscribe to incoming statements\n this.subscription = this.client.subscribe<T>(\n (statement) => this.handleStatement(statement),\n { topic2: this.topic2 },\n );\n }\n\n /**\n * Write a value to a named channel.\n *\n * If the value doesn't include a `timestamp`, one is added automatically\n * using `Date.now()`. The value is published to the statement store\n * with the channel name as the statement channel.\n *\n * @param channelName - The channel name (e.g., \"presence/peer-abc\").\n * @param value - The value to write.\n * @returns `true` if the statement was accepted by the network.\n */\n async write(channelName: string, value: T): Promise<boolean> {\n const timestamped = value.timestamp != null ? value : { ...value, timestamp: Date.now() };\n\n const options: PublishOptions = {\n channel: channelName,\n topic2: this.topic2,\n };\n\n const success = await this.client.publish(timestamped, options);\n\n if (success) {\n // Store by hash key so local writes and network echoes use the same key\n const hashKey = topicToHex(createChannel(channelName));\n this.nameToHash.set(channelName, hashKey);\n this.updateChannel(hashKey, timestamped);\n }\n\n return success;\n }\n\n /**\n * Read the latest value for a channel by its human-readable name.\n *\n * Looks up the channel hash from the name, then retrieves the value.\n * Also checks the hash directly for values received from the network\n * before any local write established the name mapping.\n *\n * @param channelName - The channel name (e.g., \"presence/peer-abc\").\n * @returns The latest value, or `undefined` if no value has been received.\n */\n read(channelName: string): T | undefined {\n const hashKey = this.nameToHash.get(channelName) ?? topicToHex(createChannel(channelName));\n return this.values.get(hashKey);\n }\n\n /**\n * Read all channel values as a read-only map.\n *\n * Keys are hex-encoded channel hashes. Use {@link read} for\n * lookup by human-readable name.\n *\n * @returns A map of channel hash to latest value.\n */\n readAll(): ReadonlyMap<string, T> {\n return this.values;\n }\n\n /**\n * Get the number of channels currently tracked.\n */\n get size(): number {\n return this.values.size;\n }\n\n /**\n * Subscribe to channel value changes.\n *\n * The callback is invoked whenever a channel value is updated\n * (either from the network or from a local write).\n *\n * @param callback - Called with the channel key (hex hash for network-received, hex hash for local writes), new value, and previous value.\n * @returns A handle to unsubscribe.\n */\n onChange(\n callback: (channelName: string, value: T, previous: T | undefined) => void,\n ): Unsubscribable {\n this.changeCallbacks.push(callback);\n return {\n unsubscribe: () => {\n const index = this.changeCallbacks.indexOf(callback);\n if (index >= 0) {\n this.changeCallbacks.splice(index, 1);\n }\n },\n };\n }\n\n /**\n * Destroy the channel store and clean up subscriptions.\n *\n * Does not destroy the underlying client.\n */\n destroy(): void {\n if (this.subscription) {\n this.subscription.unsubscribe();\n this.subscription = null;\n }\n this.values.clear();\n this.nameToHash.clear();\n this.changeCallbacks.length = 0;\n log.debug(\"ChannelStore destroyed\");\n }\n\n // ========================================================================\n // Internal\n // ========================================================================\n\n private handleStatement(statement: ReceivedStatement<T>): void {\n // We need a channel to determine the channel name.\n // Without a channel, we can't do last-write-wins deduplication.\n if (!statement.channelHex) return;\n\n // The channel hex is the blake2b of the channel name.\n // We store by the hex representation since we don't have\n // the original channel name from incoming statements.\n this.updateChannel(statement.channelHex, statement.data);\n }\n\n private updateChannel(channelName: string, value: T): void {\n const existing = this.values.get(channelName);\n\n // Last-write-wins: only update if newer\n if (existing) {\n const existingTs = existing.timestamp ?? 0;\n const newTs = value.timestamp ?? 0;\n if (newTs <= existingTs) {\n return; // Same or older value, skip (idempotent)\n }\n }\n\n const previous = existing;\n this.values.set(channelName, value);\n\n // Fire change callbacks (snapshot to handle mid-iteration unsubscribes)\n for (const callback of [...this.changeCallbacks]) {\n try {\n callback(channelName, value, previous);\n } catch (error) {\n log.error(\"onChange callback error\", {\n error: error instanceof Error ? error.message : String(error),\n });\n }\n }\n }\n}\n\nif (import.meta.vitest) {\n const { describe, test, expect, vi, beforeEach } = import.meta.vitest;\n const { configure } = await import(\"@parity/product-sdk-logger\");\n\n beforeEach(() => {\n configure({ handler: () => {} });\n });\n\n // Minimal mock of StatementStoreClient\n function createMockClient() {\n const subscribeCallbacks: Array<(stmt: ReceivedStatement<unknown>) => void> = [];\n\n return {\n subscribe: vi.fn(\n <T>(\n callback: (stmt: ReceivedStatement<T>) => void,\n _options?: { topic2?: string },\n ) => {\n subscribeCallbacks.push(callback as (stmt: ReceivedStatement<unknown>) => void);\n return {\n unsubscribe: () => {\n const idx = subscribeCallbacks.indexOf(\n callback as (stmt: ReceivedStatement<unknown>) => void,\n );\n if (idx >= 0) subscribeCallbacks.splice(idx, 1);\n },\n };\n },\n ),\n publish: vi.fn(async () => true),\n // Helper to simulate incoming statement\n _simulateStatement(stmt: ReceivedStatement<unknown>) {\n for (const cb of subscribeCallbacks) {\n cb(stmt);\n }\n },\n };\n }\n\n type TestValue = { type: string; timestamp: number };\n\n describe(\"ChannelStore\", () => {\n test(\"write publishes and updates local state\", async () => {\n const mockClient = createMockClient();\n const store = new ChannelStore<TestValue>(\n mockClient as unknown as StatementStoreClient,\n );\n\n await store.write(\"presence/abc\", { type: \"presence\", timestamp: 1000 });\n\n expect(mockClient.publish).toHaveBeenCalledOnce();\n // read by name resolves through the hash\n expect(store.read(\"presence/abc\")).toEqual({ type: \"presence\", timestamp: 1000 });\n });\n\n test(\"write adds timestamp if missing\", async () => {\n const mockClient = createMockClient();\n const store = new ChannelStore<{ type: string; timestamp?: number }>(\n mockClient as unknown as StatementStoreClient,\n );\n\n const before = Date.now();\n await store.write(\"ch\", { type: \"test\" });\n\n const calls = mockClient.publish.mock.calls as unknown[][];\n const published = calls[0][0] as { timestamp: number };\n expect(published.timestamp).toBeGreaterThanOrEqual(before);\n });\n\n test(\"write returns false on publish failure\", async () => {\n const mockClient = createMockClient();\n mockClient.publish = vi.fn(async () => false);\n const store = new ChannelStore<TestValue>(\n mockClient as unknown as StatementStoreClient,\n );\n\n const result = await store.write(\"ch\", { type: \"test\", timestamp: 1 });\n expect(result).toBe(false);\n expect(store.read(\"ch\")).toBeUndefined(); // Not stored on failure\n });\n\n test(\"read returns undefined for unknown channel\", () => {\n const mockClient = createMockClient();\n const store = new ChannelStore<TestValue>(\n mockClient as unknown as StatementStoreClient,\n );\n\n expect(store.read(\"unknown\")).toBeUndefined();\n });\n\n test(\"readAll returns all values keyed by hash\", async () => {\n const mockClient = createMockClient();\n const store = new ChannelStore<TestValue>(\n mockClient as unknown as StatementStoreClient,\n );\n\n await store.write(\"a\", { type: \"a\", timestamp: 1 });\n await store.write(\"b\", { type: \"b\", timestamp: 2 });\n\n const all = store.readAll();\n expect(all.size).toBe(2);\n });\n\n test(\"size returns channel count\", async () => {\n const mockClient = createMockClient();\n const store = new ChannelStore<TestValue>(\n mockClient as unknown as StatementStoreClient,\n );\n\n expect(store.size).toBe(0);\n await store.write(\"a\", { type: \"a\", timestamp: 1 });\n expect(store.size).toBe(1);\n await store.write(\"b\", { type: \"b\", timestamp: 2 });\n expect(store.size).toBe(2);\n });\n\n test(\"last-write-wins: newer timestamp replaces older\", async () => {\n const mockClient = createMockClient();\n const store = new ChannelStore<TestValue>(\n mockClient as unknown as StatementStoreClient,\n );\n\n await store.write(\"ch\", { type: \"old\", timestamp: 100 });\n await store.write(\"ch\", { type: \"new\", timestamp: 200 });\n\n expect(store.read(\"ch\")?.type).toBe(\"new\");\n });\n\n test(\"last-write-wins: older timestamp is ignored\", async () => {\n const mockClient = createMockClient();\n const store = new ChannelStore<TestValue>(\n mockClient as unknown as StatementStoreClient,\n );\n\n await store.write(\"ch\", { type: \"new\", timestamp: 200 });\n await store.write(\"ch\", { type: \"old\", timestamp: 100 });\n\n expect(store.read(\"ch\")?.type).toBe(\"new\");\n });\n\n test(\"onChange fires on update with previous value\", async () => {\n const mockClient = createMockClient();\n const store = new ChannelStore<TestValue>(\n mockClient as unknown as StatementStoreClient,\n );\n const onChange = vi.fn();\n store.onChange(onChange);\n\n await store.write(\"ch\", { type: \"first\", timestamp: 1 });\n expect(onChange).toHaveBeenCalledOnce();\n expect(onChange.mock.calls[0][1]).toEqual({ type: \"first\", timestamp: 1 });\n expect(onChange.mock.calls[0][2]).toBeUndefined();\n\n await store.write(\"ch\", { type: \"second\", timestamp: 2 });\n expect(onChange).toHaveBeenCalledTimes(2);\n expect(onChange.mock.calls[1][1]).toEqual({ type: \"second\", timestamp: 2 });\n expect(onChange.mock.calls[1][2]).toEqual({ type: \"first\", timestamp: 1 });\n });\n\n test(\"onChange unsubscribe stops notifications\", async () => {\n const mockClient = createMockClient();\n const store = new ChannelStore<TestValue>(\n mockClient as unknown as StatementStoreClient,\n );\n const onChange = vi.fn();\n const sub = store.onChange(onChange);\n\n await store.write(\"ch\", { type: \"first\", timestamp: 1 });\n expect(onChange).toHaveBeenCalledOnce();\n\n sub.unsubscribe();\n\n await store.write(\"ch\", { type: \"second\", timestamp: 2 });\n expect(onChange).toHaveBeenCalledOnce(); // No additional call\n });\n\n test(\"destroy cleans up\", async () => {\n const mockClient = createMockClient();\n const store = new ChannelStore<TestValue>(\n mockClient as unknown as StatementStoreClient,\n );\n\n await store.write(\"ch\", { type: \"test\", timestamp: 1 });\n store.destroy();\n\n expect(store.readAll().size).toBe(0);\n });\n\n test(\"destroy is safe when subscription is already null\", () => {\n const mockClient = createMockClient();\n const store = new ChannelStore<TestValue>(\n mockClient as unknown as StatementStoreClient,\n );\n store.destroy();\n // Second destroy — subscription is already null\n expect(() => store.destroy()).not.toThrow();\n });\n });\n}\n"]}
1
+ {"version":3,"sources":["../src/types.ts","../src/errors.ts","../src/data.ts","../src/topics.ts","../src/transport.ts","../src/statement.ts","../src/client.ts","../src/channels.ts"],"names":["log","createLogger","blake2b256"],"mappings":"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","file":"index.js","sourcesContent":["// Copyright 2026 Parity Technologies (UK) Ltd.\n// SPDX-License-Identifier: Apache-2.0\n// Re-export statement value types from the local ./statement module.\n\n/** A record published to the Statement Store. Every field is optional: a `data` payload, `topics` for routing, a `channel` for last-write-wins, an `expiry`, and a `proof` once signed. */\nexport type { Statement } from \"./statement.js\";\n\n/** A {@link Statement} with its `proof` attached and ready for submission — the only field the submission path actually requires. */\nexport type { SignedStatement } from \"./statement.js\";\n\n/** A {@link Statement} before signing — the same optional fields, minus `proof`. */\nexport type { UnsignedStatement } from \"./statement.js\";\n\n/** Signature attached to a statement — `Sr25519`/`Ed25519`/`Ecdsa` plus public key, or an on-chain witness referencing a block event. Re-exported from the truapi wire types (`{ tag }` discriminant). */\nexport type { Proof } from \"./statement.js\";\n\n/** Hex-string topic filter shape — same `\"any\"` / `matchAll` / `matchAny` structure as {@link TopicFilter}, but carries topics as `0x`-prefixed hex strings rather than the `Uint8Array`-based {@link TopicHash}. Reach for this when working with a {@link StatementTransport} directly. */\nexport type { TopicFilter as SdkTopicFilter } from \"./statement.js\";\n\nimport type { Statement, SignedStatement, TopicFilter as SdkTopicFilter } from \"./statement.js\";\n\n// ============================================================================\n// Constants\n// ============================================================================\n\n/** Maximum size of a single statement's data payload in bytes. */\nexport const MAX_STATEMENT_SIZE = 512;\n\n/** Maximum total storage per user in bytes. */\nexport const MAX_USER_TOTAL = 1024;\n\n/** Default time-to-live for published statements in seconds. */\nexport const DEFAULT_TTL_SECONDS = 30;\n\n// ============================================================================\n// Branded Types\n// ============================================================================\n\n/**\n * A 32-byte blake2b-256 hash used as a statement topic.\n *\n * Topics allow subscribers to filter statements efficiently on the network.\n * Create one with {@link createTopic}.\n */\nexport type TopicHash = Uint8Array & { readonly __brand: \"TopicHash\" };\n\n/**\n * A 32-byte blake2b-256 hash used as a statement channel identifier.\n *\n * Channels enable last-write-wins semantics: for a given channel,\n * only the most recent statement (by timestamp) is kept.\n * Create one with {@link createChannel}.\n */\nexport type ChannelHash = Uint8Array & { readonly __brand: \"ChannelHash\" };\n\n// ============================================================================\n// Topic Filtering\n// ============================================================================\n\n/**\n * Filter for statement subscriptions and queries.\n *\n * - `\"any\"` — matches all statements (no filtering).\n * - `{ matchAll: [...] }` — matches statements that have **all** listed topics.\n * - `{ matchAny: [...] }` — matches statements that have **any** listed topic.\n */\nexport type TopicFilter = \"any\" | { matchAll: TopicHash[] } | { matchAny: TopicHash[] };\n\n// ============================================================================\n// Signer & Credentials\n// ============================================================================\n\n/**\n * A function that signs a message with an Sr25519 key.\n *\n * Takes the signature material bytes and returns a 64-byte Sr25519 signature.\n * May be synchronous or asynchronous (e.g., when signing via a hardware wallet).\n */\nexport type StatementSigner = (message: Uint8Array) => Uint8Array | Promise<Uint8Array>;\n\n/**\n * An Sr25519 signer with its associated public key.\n *\n * Used by {@link StatementStoreClient.connect} to sign published statements\n * when running outside a container (local mode).\n */\nexport interface StatementSignerWithKey {\n /** 32-byte Sr25519 public key. */\n publicKey: Uint8Array;\n /** Signing function. */\n sign: StatementSigner;\n}\n\n/**\n * Credentials for connecting to the statement store.\n *\n * - **Host mode**: Inside a container, proof creation is delegated to the host\n * via the RFC-10 sponsored path (`createProofAuthorized`) — the host signs\n * with the product's allowance account, so `accountId` is no longer required\n * and is ignored if supplied (kept for backward compatibility).\n * - **Local mode**: Outside a container, statements are signed locally using the\n * provided Sr25519 signer.\n */\nexport type ConnectionCredentials =\n | { mode: \"host\"; accountId?: [string, number] }\n | { mode: \"local\"; signer: StatementSignerWithKey };\n\n// ============================================================================\n// Configuration\n// ============================================================================\n\n/**\n * Configuration for {@link StatementStoreClient}.\n *\n * The client uses the Host API's native statement store protocol.\n * The SDK is designed to run exclusively inside a host container.\n */\nexport interface StatementStoreConfig {\n /**\n * Application namespace used as the primary topic (topic1).\n *\n * All statements published by this client are tagged with `blake2b(appName)`.\n * Subscribers filter on this topic to receive only relevant statements.\n *\n * @example \"ss-webrtc\", \"mark3t-presence\", \"my-app\"\n */\n appName: string;\n\n /**\n * Default time-to-live for published statements in seconds.\n *\n * Statements automatically expire after this duration.\n * Can be overridden per-publish via {@link PublishOptions.ttlSeconds}.\n *\n * @default 30\n */\n defaultTtlSeconds?: number;\n\n /**\n * Provide a custom transport instead of auto-detection.\n *\n * When set, the client uses this transport directly.\n * Useful for testing or advanced BYOD setups.\n */\n transport?: StatementTransport;\n}\n\n// ============================================================================\n// Publish & Subscribe\n// ============================================================================\n\n/**\n * Options for publishing a single statement.\n */\nexport interface PublishOptions {\n /**\n * Channel name for last-write-wins semantics.\n *\n * When provided, the statement is tagged with `blake2b(channel)`.\n * For a given channel, only the most recent statement is kept.\n *\n * @example \"presence/peer-abc123\", \"handshake/alice-bob\"\n */\n channel?: string;\n\n /**\n * Secondary topic for additional filtering.\n *\n * Hashed with blake2b and set as topic2. Useful for scoping\n * statements to a specific room, document, or context.\n *\n * @example \"doc-abc123\", \"room-456\"\n */\n topic2?: string;\n\n /**\n * Time-to-live in seconds. Overrides {@link StatementStoreConfig.defaultTtlSeconds}.\n */\n ttlSeconds?: number;\n\n /**\n * Decryption key hint (32 bytes).\n *\n * Used by the `statement_posted` RPC method to filter statements.\n * Typically set to the blake2b hash of the room or document ID.\n */\n decryptionKey?: Uint8Array;\n}\n\n/**\n * A received statement with typed data and metadata.\n *\n * @typeParam T - The parsed data type (decoded from JSON).\n */\nexport interface ReceivedStatement<T = unknown> {\n /** Parsed data payload. */\n data: T;\n /** Signer's public key hex (from proof), if present. */\n signerHex?: string;\n /** Channel hex, if present. */\n channelHex?: string;\n /** Topics array (hex strings). */\n topics: string[];\n /** Combined expiry value (upper 32 bits = timestamp, lower 32 bits = sequence). */\n expiry?: bigint;\n /** The full statement from the transport for advanced inspection. */\n raw: Statement;\n}\n\n// ============================================================================\n// Transport\n// ============================================================================\n\n/** Handle returned by subscription methods. Call `unsubscribe()` to stop receiving events. */\nexport interface Unsubscribable {\n unsubscribe: () => void;\n}\n\n/**\n * Low-level transport interface for statement store communication.\n *\n * Uses **HostTransport** — the Host API's native binary protocol.\n *\n * Most consumers should use {@link StatementStoreClient} instead of this interface directly.\n */\nexport interface StatementTransport {\n /**\n * Subscribe to statements matching a topic filter.\n *\n * @param filter - sdk-statement topic filter.\n * @param onStatements - Called with batches of received statements.\n * @param onError - Called when the subscription encounters an error.\n * @returns A handle to unsubscribe.\n */\n subscribe(\n filter: SdkTopicFilter,\n onStatements: (statements: Statement[]) => void,\n onError: (error: Error) => void,\n ): Unsubscribable;\n\n /**\n * Sign and submit a statement.\n *\n * - **Host mode**: delegates to the host API's `createProof` then `submit`.\n * - **Local mode**: signs locally using `getStatementSigner` from sdk-statement, then submits.\n *\n * @param statement - The unsigned statement to sign and submit.\n * @param credentials - Signing credentials (host accountId or local signer).\n */\n signAndSubmit(statement: Statement, credentials: ConnectionCredentials): Promise<void>;\n\n /**\n * Query existing statements from the store.\n *\n * Note: The host API subscription replays initial state, so this may not be needed.\n */\n query?(filter: SdkTopicFilter): Promise<Statement[]>;\n\n /** Destroy the transport and release all resources. */\n destroy(): void;\n}\n","// Copyright 2026 Parity Technologies (UK) Ltd.\n// SPDX-License-Identifier: Apache-2.0\nimport { MAX_STATEMENT_SIZE } from \"./types.js\";\n\n/**\n * Base class for all statement store errors.\n *\n * Use `instanceof StatementStoreError` to catch any error originating\n * from the statement store package.\n */\nexport class StatementStoreError extends Error {\n constructor(message: string, options?: ErrorOptions) {\n super(message, options);\n this.name = \"StatementStoreError\";\n }\n}\n\n/**\n * A SCALE encoding or decoding operation failed.\n *\n * Thrown when statement bytes cannot be parsed (corrupt data, unknown field tags)\n * or when encoding produces invalid output.\n */\nexport class StatementEncodingError extends StatementStoreError {\n constructor(message: string, options?: ErrorOptions) {\n super(`Encoding error: ${message}`, options);\n this.name = \"StatementEncodingError\";\n }\n}\n\n/**\n * The statement store node rejected a submitted statement.\n *\n * Carries the raw RPC response detail for programmatic inspection.\n */\nexport class StatementSubmitError extends StatementStoreError {\n /** The raw response from the RPC call. */\n readonly detail: unknown;\n\n constructor(detail: unknown) {\n super(`Statement submission rejected: ${JSON.stringify(detail)}`);\n this.name = \"StatementSubmitError\";\n this.detail = detail;\n }\n}\n\n/**\n * Failed to set up or maintain a statement subscription.\n *\n * This is a non-fatal error — the client falls back to polling\n * when subscriptions are unavailable.\n */\nexport class StatementSubscriptionError extends StatementStoreError {\n constructor(message: string, options?: ErrorOptions) {\n super(`Subscription error: ${message}`, options);\n this.name = \"StatementSubscriptionError\";\n }\n}\n\n/**\n * Failed to connect to the statement store transport.\n *\n * Thrown when the WebSocket connection cannot be established\n * or the chain-client's bulletin chain is not connected.\n */\nexport class StatementConnectionError extends StatementStoreError {\n constructor(message: string, options?: ErrorOptions) {\n super(`Connection error: ${message}`, options);\n this.name = \"StatementConnectionError\";\n }\n}\n\n/**\n * The statement data payload exceeds the maximum allowed size.\n *\n * The statement store protocol limits individual statement data\n * to {@link MAX_STATEMENT_SIZE} bytes (512 bytes).\n */\nexport class StatementDataTooLargeError extends StatementStoreError {\n /** The actual size of the data in bytes. */\n readonly actualSize: number;\n /** The maximum allowed size in bytes. */\n readonly maxSize: number;\n\n constructor(actualSize: number, maxSize: number = MAX_STATEMENT_SIZE) {\n super(`Statement data too large: ${actualSize} bytes exceeds maximum of ${maxSize} bytes`);\n this.name = \"StatementDataTooLargeError\";\n this.actualSize = actualSize;\n this.maxSize = maxSize;\n }\n}\n\nif (import.meta.vitest) {\n const { describe, test, expect } = import.meta.vitest;\n\n describe(\"StatementStoreError hierarchy\", () => {\n test(\"StatementStoreError is instanceof Error\", () => {\n const err = new StatementStoreError(\"test\");\n expect(err).toBeInstanceOf(Error);\n expect(err).toBeInstanceOf(StatementStoreError);\n expect(err.name).toBe(\"StatementStoreError\");\n expect(err.message).toBe(\"test\");\n });\n\n test(\"StatementEncodingError\", () => {\n const err = new StatementEncodingError(\"bad field tag\");\n expect(err).toBeInstanceOf(StatementStoreError);\n expect(err).toBeInstanceOf(Error);\n expect(err.name).toBe(\"StatementEncodingError\");\n expect(err.message).toContain(\"bad field tag\");\n });\n\n test(\"StatementEncodingError preserves cause\", () => {\n const cause = new Error(\"original\");\n const err = new StatementEncodingError(\"wrap\", { cause });\n expect(err.cause).toBe(cause);\n });\n\n test(\"StatementSubmitError\", () => {\n const detail = { status: \"rejected\", reason: \"bad proof\" };\n const err = new StatementSubmitError(detail);\n expect(err).toBeInstanceOf(StatementStoreError);\n expect(err.name).toBe(\"StatementSubmitError\");\n expect(err.detail).toBe(detail);\n expect(err.message).toContain(\"rejected\");\n });\n\n test(\"StatementSubscriptionError\", () => {\n const err = new StatementSubscriptionError(\"not supported\");\n expect(err).toBeInstanceOf(StatementStoreError);\n expect(err.name).toBe(\"StatementSubscriptionError\");\n expect(err.message).toContain(\"not supported\");\n });\n\n test(\"StatementConnectionError\", () => {\n const err = new StatementConnectionError(\"timeout\");\n expect(err).toBeInstanceOf(StatementStoreError);\n expect(err.name).toBe(\"StatementConnectionError\");\n expect(err.message).toContain(\"timeout\");\n });\n\n test(\"StatementDataTooLargeError\", () => {\n const err = new StatementDataTooLargeError(600);\n expect(err).toBeInstanceOf(StatementStoreError);\n expect(err.name).toBe(\"StatementDataTooLargeError\");\n expect(err.actualSize).toBe(600);\n expect(err.maxSize).toBe(512);\n expect(err.message).toContain(\"600\");\n expect(err.message).toContain(\"512\");\n });\n\n test(\"StatementDataTooLargeError with custom max\", () => {\n const err = new StatementDataTooLargeError(2000, 1024);\n expect(err.actualSize).toBe(2000);\n expect(err.maxSize).toBe(1024);\n });\n\n test(\"all errors are catchable via base class\", () => {\n const errors = [\n new StatementEncodingError(\"test\"),\n new StatementSubmitError(\"test\"),\n new StatementSubscriptionError(\"test\"),\n new StatementConnectionError(\"test\"),\n new StatementDataTooLargeError(600),\n ];\n for (const err of errors) {\n expect(err).toBeInstanceOf(StatementStoreError);\n }\n });\n });\n}\n","// Copyright 2026 Parity Technologies (UK) Ltd.\n// SPDX-License-Identifier: Apache-2.0\nimport { StatementDataTooLargeError, StatementEncodingError } from \"./errors.js\";\nimport { MAX_STATEMENT_SIZE } from \"./types.js\";\n\n/** Convert a hex string (with or without 0x prefix) to bytes. */\nexport function fromHex(hex: string): Uint8Array {\n const clean = hex.startsWith(\"0x\") ? hex.slice(2) : hex;\n const bytes = new Uint8Array(clean.length / 2);\n for (let i = 0; i < bytes.length; i++) {\n bytes[i] = Number.parseInt(clean.substring(i * 2, i * 2 + 2), 16);\n }\n return bytes;\n}\n\n/** Convert bytes to a hex string with 0x prefix. */\nexport function toHex(bytes: Uint8Array): string {\n let hex = \"0x\";\n for (let i = 0; i < bytes.length; i++) {\n hex += bytes[i].toString(16).padStart(2, \"0\");\n }\n return hex;\n}\n\n/**\n * Encode a value as a JSON-serialized data payload for a statement.\n *\n * Serializes the value as JSON and encodes to UTF-8 bytes.\n * Throws {@link StatementDataTooLargeError} if the result exceeds\n * {@link MAX_STATEMENT_SIZE} bytes.\n *\n * @typeParam T - The type of value being encoded.\n * @param value - The value to serialize.\n * @returns UTF-8 encoded JSON bytes.\n * @throws {StatementDataTooLargeError} If the encoded data exceeds 512 bytes.\n */\nexport function encodeData<T>(value: T): Uint8Array {\n const json = JSON.stringify(value);\n const bytes = new TextEncoder().encode(json);\n if (bytes.length > MAX_STATEMENT_SIZE) {\n throw new StatementDataTooLargeError(bytes.length);\n }\n return bytes;\n}\n\n/**\n * Decode a JSON-serialized data payload from statement bytes.\n *\n * @typeParam T - The expected parsed type.\n * @param bytes - UTF-8 encoded JSON bytes.\n * @returns The parsed value.\n * @throws {StatementEncodingError} If the bytes are not valid UTF-8 or valid JSON.\n */\nexport function decodeData<T>(bytes: Uint8Array): T {\n try {\n const json = new TextDecoder(\"utf-8\", { fatal: true }).decode(bytes);\n return JSON.parse(json) as T;\n } catch (error) {\n throw new StatementEncodingError(\"Failed to decode JSON data\", { cause: error });\n }\n}\n\nif (import.meta.vitest) {\n const { describe, test, expect } = import.meta.vitest;\n\n describe(\"encodeData / decodeData\", () => {\n test(\"round-trips JSON object\", () => {\n const original = { type: \"presence\", peerId: \"abc123\", timestamp: 1234 };\n const encoded = encodeData(original);\n const decoded = decodeData<typeof original>(encoded);\n expect(decoded).toEqual(original);\n });\n\n test(\"round-trips string\", () => {\n const encoded = encodeData(\"hello\");\n expect(decodeData<string>(encoded)).toBe(\"hello\");\n });\n\n test(\"round-trips number\", () => {\n const encoded = encodeData(42);\n expect(decodeData<number>(encoded)).toBe(42);\n });\n\n test(\"throws StatementDataTooLargeError for oversized data\", () => {\n const large = { data: \"x\".repeat(600) };\n expect(() => encodeData(large)).toThrow(StatementDataTooLargeError);\n });\n\n test(\"allows data at exactly MAX_STATEMENT_SIZE\", () => {\n const str = \"a\".repeat(510);\n const encoded = encodeData(str);\n expect(encoded.length).toBe(512);\n });\n\n test(\"throws StatementEncodingError for invalid JSON bytes\", () => {\n const invalid = new TextEncoder().encode(\"{not valid json\");\n expect(() => decodeData(invalid)).toThrow(StatementEncodingError);\n });\n\n test(\"throws StatementEncodingError for non-UTF-8 bytes\", () => {\n const invalid = new Uint8Array([0xff, 0xfe, 0xfd]);\n expect(() => decodeData(invalid)).toThrow(StatementEncodingError);\n });\n });\n\n describe(\"toHex / fromHex\", () => {\n test(\"round-trips bytes\", () => {\n const bytes = new Uint8Array([0xde, 0xad, 0xbe, 0xef]);\n const hex = toHex(bytes);\n expect(hex).toBe(\"0xdeadbeef\");\n expect(fromHex(hex)).toEqual(bytes);\n });\n\n test(\"handles hex without 0x prefix\", () => {\n const bytes = fromHex(\"cafebabe\");\n expect(bytes).toEqual(new Uint8Array([0xca, 0xfe, 0xba, 0xbe]));\n });\n\n test(\"handles empty input\", () => {\n expect(toHex(new Uint8Array(0))).toBe(\"0x\");\n expect(fromHex(\"0x\")).toEqual(new Uint8Array(0));\n });\n });\n}\n","// Copyright 2026 Parity Technologies (UK) Ltd.\n// SPDX-License-Identifier: Apache-2.0\nimport { blake2b256 } from \"@parity/product-sdk-utils\";\n\nimport type { ChannelHash, TopicFilter, TopicHash, SdkTopicFilter } from \"./types.js\";\n\n/**\n * Create a 32-byte topic hash from a human-readable string.\n *\n * Uses blake2b-256 to hash the UTF-8 encoded string into a deterministic\n * 32-byte topic identifier. Statements are tagged with topics so subscribers\n * can filter efficiently on the network.\n *\n * @param name - A human-readable topic name (e.g., \"ss-webrtc\", \"my-app\").\n * @returns A branded 32-byte topic hash.\n *\n * @example\n * ```ts\n * const topic = createTopic(\"ss-webrtc\");\n * // Use in subscriptions and publish options\n * ```\n */\nexport function createTopic(name: string): TopicHash {\n const bytes = new TextEncoder().encode(name);\n return blake2b256(bytes) as TopicHash;\n}\n\n/**\n * Create a 32-byte channel hash from a human-readable channel name.\n *\n * Channels enable last-write-wins semantics: for a given channel,\n * only the most recent statement (by timestamp) is retained.\n *\n * @param name - A human-readable channel name (e.g., \"presence/peer-abc\").\n * @returns A branded 32-byte channel hash.\n *\n * @example\n * ```ts\n * const channel = createChannel(\"presence/peer-abc123\");\n * ```\n */\nexport function createChannel(name: string): ChannelHash {\n const bytes = new TextEncoder().encode(name);\n return blake2b256(bytes) as ChannelHash;\n}\n\n/**\n * Convert a topic or channel hash to a hex string (with 0x prefix).\n *\n * @param hash - A 32-byte topic or channel hash.\n * @returns Hex string with \"0x\" prefix.\n */\nexport function topicToHex(hash: Uint8Array): `0x${string}` {\n let hex = \"0x\";\n for (let i = 0; i < hash.length; i++) {\n hex += hash[i].toString(16).padStart(2, \"0\");\n }\n return hex as `0x${string}`;\n}\n\n/**\n * Compare two topic or channel hashes for byte equality.\n *\n * @param a - First hash.\n * @param b - Second hash.\n * @returns True if the hashes are identical.\n */\nexport function topicsEqual(a: Uint8Array, b: Uint8Array): boolean {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (a[i] !== b[i]) return false;\n }\n return true;\n}\n\n/**\n * Serialize a {@link TopicFilter} into the JSON-RPC format expected by statement store nodes.\n *\n * - `\"any\"` is passed through as the string `\"any\"`.\n * - `{ matchAll: [...] }` becomes `{ matchAll: [\"0x...\", ...] }` with hex-encoded topics.\n * - `{ matchAny: [...] }` becomes `{ matchAny: [\"0x...\", ...] }` with hex-encoded topics.\n *\n * @param filter - The topic filter to serialize.\n * @returns A JSON-RPC compatible filter value.\n */\nexport function serializeTopicFilter(filter: TopicFilter): SdkTopicFilter {\n if (filter === \"any\") return \"any\";\n\n if (\"matchAll\" in filter) {\n return { matchAll: filter.matchAll.map(topicToHex) };\n }\n\n return { matchAny: filter.matchAny.map(topicToHex) };\n}\n\nif (import.meta.vitest) {\n const { describe, test, expect } = import.meta.vitest;\n\n describe(\"createTopic\", () => {\n test(\"produces a 32-byte hash\", () => {\n const topic = createTopic(\"test\");\n expect(topic).toBeInstanceOf(Uint8Array);\n expect(topic.length).toBe(32);\n });\n\n test(\"is deterministic (same input = same hash)\", () => {\n const a = createTopic(\"ss-webrtc\");\n const b = createTopic(\"ss-webrtc\");\n expect(topicsEqual(a, b)).toBe(true);\n });\n\n test(\"different inputs produce different hashes\", () => {\n const a = createTopic(\"topic-a\");\n const b = createTopic(\"topic-b\");\n expect(topicsEqual(a, b)).toBe(false);\n });\n\n test(\"empty string produces a valid hash\", () => {\n const topic = createTopic(\"\");\n expect(topic.length).toBe(32);\n });\n });\n\n describe(\"createChannel\", () => {\n test(\"produces a 32-byte hash\", () => {\n const channel = createChannel(\"presence/peer-abc\");\n expect(channel).toBeInstanceOf(Uint8Array);\n expect(channel.length).toBe(32);\n });\n\n test(\"same input as createTopic produces same bytes\", () => {\n const topic = createTopic(\"test-name\");\n const channel = createChannel(\"test-name\");\n expect(topicsEqual(topic, channel)).toBe(true);\n });\n });\n\n describe(\"topicToHex\", () => {\n test(\"converts to hex with 0x prefix\", () => {\n const hash = new Uint8Array(32);\n hash[0] = 0xab;\n hash[1] = 0xcd;\n const hex = topicToHex(hash);\n expect(hex).toMatch(/^0x/);\n expect(hex).toBe(`0xabcd${\"00\".repeat(30)}`);\n });\n\n test(\"round-trips through hex encoding\", () => {\n const topic = createTopic(\"round-trip-test\");\n const hex = topicToHex(topic);\n expect(hex.length).toBe(2 + 64); // \"0x\" + 64 hex chars\n });\n\n test(\"pads single-digit bytes with leading zero\", () => {\n const hash = new Uint8Array([0x0a]);\n expect(topicToHex(hash)).toBe(\"0x0a\");\n });\n });\n\n describe(\"topicsEqual\", () => {\n test(\"returns true for identical hashes\", () => {\n const a = createTopic(\"same\");\n const b = createTopic(\"same\");\n expect(topicsEqual(a, b)).toBe(true);\n });\n\n test(\"returns false for different hashes\", () => {\n const a = createTopic(\"alpha\");\n const b = createTopic(\"beta\");\n expect(topicsEqual(a, b)).toBe(false);\n });\n\n test(\"returns false for different lengths\", () => {\n const a = new Uint8Array(32);\n const b = new Uint8Array(16);\n expect(topicsEqual(a, b)).toBe(false);\n });\n\n test(\"returns true for empty arrays\", () => {\n expect(topicsEqual(new Uint8Array(0), new Uint8Array(0))).toBe(true);\n });\n });\n\n describe(\"serializeTopicFilter\", () => {\n test(\"serializes 'any' as string\", () => {\n expect(serializeTopicFilter(\"any\")).toBe(\"any\");\n });\n\n test(\"serializes matchAll with hex topics\", () => {\n const topics = [createTopic(\"a\"), createTopic(\"b\")];\n const result = serializeTopicFilter({ matchAll: topics }) as {\n matchAll: string[];\n };\n expect(result.matchAll).toHaveLength(2);\n expect(result.matchAll[0]).toMatch(/^0x[0-9a-f]{64}$/);\n expect(result.matchAll[1]).toMatch(/^0x[0-9a-f]{64}$/);\n });\n\n test(\"serializes matchAny with hex topics\", () => {\n const topics = [createTopic(\"x\")];\n const result = serializeTopicFilter({ matchAny: topics }) as {\n matchAny: string[];\n };\n expect(result.matchAny).toHaveLength(1);\n expect(result.matchAny[0]).toMatch(/^0x[0-9a-f]{64}$/);\n });\n\n test(\"matchAll preserves order\", () => {\n const topicA = createTopic(\"first\");\n const topicB = createTopic(\"second\");\n const result = serializeTopicFilter({ matchAll: [topicA, topicB] }) as {\n matchAll: string[];\n };\n expect(result.matchAll[0]).toBe(topicToHex(topicA));\n expect(result.matchAll[1]).toBe(topicToHex(topicB));\n });\n });\n}\n","// Copyright 2026 Parity Technologies (UK) Ltd.\n// SPDX-License-Identifier: Apache-2.0\nimport type {\n HostStatementStore,\n SignedStatement as HostSignedStatement,\n Statement as HostStatement,\n StatementTopicFilter,\n} from \"@parity/product-sdk-host\";\nimport { createLogger } from \"@parity/product-sdk-logger\";\n\nimport { fromHex, toHex } from \"./data.js\";\nimport { StatementConnectionError, StatementSubscriptionError } from \"./errors.js\";\nimport type { Statement, TopicFilter as SdkTopicFilter } from \"./statement.js\";\nimport type { ConnectionCredentials, StatementTransport, Unsubscribable } from \"./types.js\";\n\nconst log = createLogger(\"statement-store:transport\");\n\n// ============================================================================\n// Host Transport — uses the Host API's native binary protocol\n// ============================================================================\n\n/**\n * Statement transport that uses the Host API inside containers.\n *\n * Communicates through the host's native `remote_statement_store_*` protocol\n * (now via `@parity/truapi`), which bypasses JSON-RPC entirely. Submission uses\n * the RFC-10 sponsored path (`createProofAuthorized`): the host signs with the\n * product's allowance account, so no per-call account id is required.\n */\nclass HostTransport implements StatementTransport {\n private readonly store: HostStatementStore;\n\n constructor(store: HostStatementStore) {\n this.store = store;\n }\n\n subscribe(\n filter: SdkTopicFilter,\n onStatements: (statements: Statement[]) => void,\n onError: (error: Error) => void,\n ): Unsubscribable {\n const hostFilter = sdkFilterToHost(filter);\n\n try {\n const sub = this.store.subscribe(hostFilter, (page) => {\n onStatements(page.statements.map(hostSignedStatementToSdk));\n });\n\n // Host-side teardown (transport close → `error`, host interrupt →\n // `complete`) is delivered only through `onInterrupt`; surface it so the\n // consumer learns the stream ended instead of silently waiting forever.\n sub.onInterrupt((reason) => {\n const msg =\n reason instanceof Error\n ? reason.message\n : \"Host ended the statement subscription\";\n log.warn(\"Host subscription interrupted\", { error: msg });\n onError(new StatementSubscriptionError(msg));\n });\n\n log.info(\"Host subscription active\");\n\n return {\n unsubscribe: () => sub.unsubscribe(),\n };\n } catch (error) {\n const msg = error instanceof Error ? error.message : String(error);\n log.warn(\"Host subscription failed\", { error: msg });\n onError(new StatementSubscriptionError(msg));\n return { unsubscribe: () => {} };\n }\n }\n\n async signAndSubmit(statement: Statement, credentials: ConnectionCredentials): Promise<void> {\n if (credentials.mode !== \"host\") {\n throw new StatementConnectionError(\n \"HostTransport requires host credentials. Use { mode: 'host' } to connect.\",\n );\n }\n\n const hostStatement = sdkStatementToHost(statement);\n // RFC-10 sponsored path: the host signs with the product's allowance\n // account, so no per-call account id is needed.\n const proof = await this.store.createProofAuthorized(hostStatement);\n const signedStatement: HostSignedStatement = { ...hostStatement, proof };\n await this.store.submit(signedStatement);\n\n log.debug(\"Statement submitted via host\");\n }\n\n destroy(): void {\n // Host owns the transport — nothing to clean up\n }\n}\n\n// ============================================================================\n// Transport Factory\n// ============================================================================\n\n/**\n * Create a statement store transport.\n *\n * Uses the Host API via `@parity/product-sdk-host` — the container's native\n * statement store protocol (binary, not JSON-RPC). This is the only supported path.\n *\n * @throws {StatementConnectionError} If the host statement store is unavailable.\n */\nexport async function createTransport(): Promise<StatementTransport> {\n const { getStatementStore } = await import(\"@parity/product-sdk-host\");\n const store = await getStatementStore();\n\n if (!store) {\n throw new StatementConnectionError(\n \"Host statement store unavailable. Ensure you are running inside a host container (Polkadot Browser / Desktop).\",\n );\n }\n\n log.info(\"Using host API statement store transport\");\n return new HostTransport(store);\n}\n\n// ============================================================================\n// Host ↔ SDK Type Bridge\n//\n// Both sides now use `0x`-prefixed hex strings for topics/channel/decryptionKey\n// and bigint expiry, so those fields pass through unchanged. Only two things\n// differ: `data` (this package keeps raw `Uint8Array`; the host uses hex) and\n// the proof discriminant (`{ type: \"sr25519\" }` here vs `{ tag: \"Sr25519\" }` on\n// the host).\n// ============================================================================\n\n/** Convert an SDK TopicFilter → host StatementTopicFilter (both hex topics). */\nfunction sdkFilterToHost(filter: SdkTopicFilter): StatementTopicFilter {\n if (filter === \"any\") {\n // The host API has no \"match anything\" variant — express it as\n // \"match any of zero topics\", which the host treats as a wildcard.\n return { matchAny: [] };\n }\n if (\"matchAll\" in filter) {\n return { matchAll: filter.matchAll };\n }\n return { matchAny: filter.matchAny };\n}\n\n/**\n * Convert a host SignedStatement → SDK Statement. The shapes are identical apart\n * from `data` (the wire type carries hex; this SDK layer decodes to bytes), so\n * everything else passes through unchanged.\n */\nfunction hostSignedStatementToSdk(hostStmt: HostSignedStatement): Statement {\n return {\n ...hostStmt,\n data: hostStmt.data !== undefined ? fromHex(hostStmt.data) : undefined,\n };\n}\n\n/** Convert an SDK Statement (bytes `data`) → host Statement (hex `data`). */\nfunction sdkStatementToHost(stmt: Statement): HostStatement {\n return {\n ...stmt,\n data: stmt.data !== undefined ? (toHex(stmt.data) as `0x${string}`) : undefined,\n };\n}\n","// Copyright 2026 Parity Technologies (UK) Ltd.\n// SPDX-License-Identifier: Apache-2.0\n/**\n * Statement Store value types and helpers.\n *\n * The statement shapes are **derived** from the in-house `@parity/truapi` wire\n * types (re-exported by `@parity/product-sdk-host`) rather than hand-redefined,\n * so changes to the protocol's statement definition propagate automatically.\n * The single intentional difference is `data`: this SDK layer works with decoded\n * `Uint8Array` bytes, whereas the wire type carries a hex string. {@link Proof}\n * and {@link Topic} are re-exported verbatim. The host transport bridges the\n * `data` field (and the ergonomic {@link TopicFilter}).\n *\n * @module\n */\n\nimport type {\n SignedStatement as WireSignedStatement,\n Statement as WireStatement,\n} from \"@parity/product-sdk-host\";\n\n/** A `0x`-prefixed hex string. */\nexport type Hex = `0x${string}`;\n\n/**\n * Signature attached to a statement — `Sr25519`/`Ed25519`/`Ecdsa` plus public\n * key, or an `OnChain` witness referencing a block event. Re-exported from the\n * truapi wire types (`{ tag }` discriminant, hex fields).\n */\nexport type { StatementProof as Proof, Topic } from \"@parity/product-sdk-host\";\n\n/**\n * A record published to the Statement Store. Derived from the truapi wire\n * {@link WireStatement}, overriding only `data` to carry decoded `Uint8Array`\n * bytes instead of a hex string. Other fields (`topics`, `channel`,\n * `decryptionKey`, `expiry`, `proof`) inherit from the wire type.\n */\nexport type Statement = Omit<WireStatement, \"data\"> & { data?: Uint8Array };\n\n/** A {@link Statement} before signing — the same fields, minus `proof`. */\nexport type UnsignedStatement = Omit<Statement, \"proof\">;\n\n/** A {@link Statement} with its `proof` attached and ready for submission. Derived from the truapi wire signed statement, with decoded `data`. */\nexport type SignedStatement = Omit<WireSignedStatement, \"data\"> & { data?: Uint8Array };\n\n/**\n * Filter for subscribing to statements.\n *\n * - `\"any\"` — match all statements.\n * - `{ matchAll }` — statement must contain **every** listed topic.\n * - `{ matchAny }` — statement must contain **at least one** listed topic.\n */\nexport type TopicFilter = \"any\" | { matchAll: Hex[] } | { matchAny: Hex[] };\n\n/**\n * Create an expiry value from a timestamp and sequence number.\n *\n * Packs the Unix timestamp (seconds) into the upper 32 bits and the sequence\n * number into the lower 32 bits of a `u64`.\n *\n * @param expirationTimestampSecs - Expiration time in seconds since the UNIX epoch.\n * @param sequenceNumber - Sequence number for ordering within a second (0–4294967295).\n */\nexport function createExpiry(expirationTimestampSecs: number, sequenceNumber = 0): bigint {\n if (sequenceNumber < 0 || sequenceNumber > 4_294_967_295) {\n throw new RangeError(\"sequenceNumber must be 0-4294967295\");\n }\n return (BigInt(expirationTimestampSecs) << 32n) | BigInt(sequenceNumber);\n}\n\nif (import.meta.vitest) {\n const { test, expect } = import.meta.vitest;\n\n test(\"createExpiry packs timestamp into the upper 32 bits and sequence into the lower\", () => {\n expect(createExpiry(1, 0)).toBe(1n << 32n);\n expect(createExpiry(0, 5)).toBe(5n);\n expect(createExpiry(2, 3)).toBe((2n << 32n) | 3n);\n });\n\n test(\"createExpiry rejects out-of-range sequence numbers\", () => {\n expect(() => createExpiry(1, -1)).toThrow(RangeError);\n expect(() => createExpiry(1, 4_294_967_296)).toThrow(RangeError);\n });\n}\n","// Copyright 2026 Parity Technologies (UK) Ltd.\n// SPDX-License-Identifier: Apache-2.0\nimport { createLogger } from \"@parity/product-sdk-logger\";\nimport { blake2b256 } from \"@parity/product-sdk-utils\";\n\nimport { decodeData, encodeData, toHex } from \"./data.js\";\nimport { StatementConnectionError } from \"./errors.js\";\nimport { createChannel, createTopic, serializeTopicFilter, topicToHex } from \"./topics.js\";\nimport { createTransport } from \"./transport.js\";\nimport type {\n ConnectionCredentials,\n PublishOptions,\n ReceivedStatement,\n StatementSignerWithKey,\n StatementStoreConfig,\n StatementTransport,\n Unsubscribable,\n} from \"./types.js\";\nimport { DEFAULT_TTL_SECONDS } from \"./types.js\";\n\nimport { createExpiry } from \"./statement.js\";\nimport type { Statement } from \"./statement.js\";\nimport type { SdkTopicFilter } from \"./types.js\";\n\nconst log = createLogger(\"statement-store\");\n\n/**\n * High-level client for the Polkadot Statement Store.\n *\n * Provides a simple publish/subscribe API over the ephemeral statement store,\n * handling topic management and signing through the host API.\n *\n * The SDK is designed to run exclusively inside a host container.\n *\n * @example\n * ```ts\n * import { StatementStoreClient } from \"@parity/product-sdk-statement-store\";\n *\n * // Inside a container (host mode)\n * const client = new StatementStoreClient({ appName: \"my-app\" });\n * await client.connect({ mode: \"host\", accountId: [\"5Grw...\", 42] });\n *\n * // Publish\n * await client.publish({ type: \"presence\", peerId: \"abc\" }, {\n * channel: \"presence/abc\",\n * topic2: \"room-123\",\n * });\n *\n * // Subscribe\n * client.subscribe<{ type: string }>(statement => {\n * console.log(statement.data.type);\n * });\n *\n * // Cleanup\n * client.destroy();\n * ```\n */\nexport class StatementStoreClient {\n private readonly config: Required<Pick<StatementStoreConfig, \"appName\" | \"defaultTtlSeconds\">> &\n Pick<StatementStoreConfig, \"transport\">;\n\n private transport: StatementTransport | null = null;\n private credentials: ConnectionCredentials | null = null;\n private subscription: Unsubscribable | null = null;\n private callbacks: Array<(statement: ReceivedStatement<unknown>) => void> = [];\n private connected = false;\n private connectPromise: Promise<void> | null = null;\n /** Set by destroy() so doConnect() can abort cleanly if destroy races with an in-flight connect. */\n private destroyed = false;\n\n /**\n * Track seen statements by channel hex to avoid re-delivering the same statement.\n * Maps channel hex (or data hash) to the expiry value.\n */\n private seen = new Map<string, bigint>();\n\n /** Monotonic counter to ensure unique sequence numbers even within the same millisecond. */\n private sequenceCounter = 0;\n\n /** Cached hex topic string for the app name, used as the primary subscription topic. */\n private readonly appTopicHex: string;\n\n constructor(config: StatementStoreConfig) {\n this.config = {\n appName: config.appName,\n defaultTtlSeconds: config.defaultTtlSeconds ?? DEFAULT_TTL_SECONDS,\n transport: config.transport,\n };\n this.appTopicHex = topicToHex(createTopic(config.appName));\n }\n\n /**\n * Connect to the statement store and start receiving statements.\n *\n * @param credentials - Connection credentials (host accountId or local signer).\n * @throws {StatementConnectionError} If the transport cannot be established.\n */\n async connect(credentials: ConnectionCredentials): Promise<void>;\n /** @deprecated Use `connect({ mode: \"local\", signer })` instead. */\n async connect(signer: StatementSignerWithKey): Promise<void>;\n async connect(arg: ConnectionCredentials | StatementSignerWithKey): Promise<void> {\n if (this.destroyed) {\n throw new StatementConnectionError(\n \"Cannot connect: client has been destroyed. Create a new instance.\",\n );\n }\n if (this.connected) {\n log.warn(\"Already connected, ignoring duplicate connect()\");\n return;\n }\n if (this.connectPromise) {\n return this.connectPromise;\n }\n\n const credentials: ConnectionCredentials =\n \"mode\" in arg ? arg : { mode: \"local\", signer: arg };\n\n this.connectPromise = this.doConnect(credentials).finally(() => {\n this.connectPromise = null;\n });\n return this.connectPromise;\n }\n\n /* @integration */\n private async doConnect(credentials: ConnectionCredentials): Promise<void> {\n this.credentials = credentials;\n const transport = this.config.transport ?? (await createTransport());\n\n // destroy() may have been called while we were awaiting createTransport().\n // If so, clean up the newly-created transport (if we own it) instead of leaking.\n if (this.destroyed) {\n if (transport !== this.config.transport) {\n transport.destroy();\n }\n return;\n }\n\n this.transport = transport;\n\n try {\n log.info(\"Connected\", { appName: this.config.appName });\n\n this.startSubscription();\n this.connected = true;\n } catch (error) {\n this.destroy();\n throw error;\n }\n }\n\n /**\n * Publish typed data to the statement store.\n *\n * @typeParam T - The type of data being published.\n * @param data - The value to publish (must be JSON-serializable, max 512 bytes).\n * @param options - Optional channel, topic2, TTL, and decryption key overrides.\n * @returns `true` if accepted, `false` if rejected or errored.\n * @throws {StatementConnectionError} If not connected.\n * @throws {StatementDataTooLargeError} If the encoded data exceeds 512 bytes.\n */\n async publish<T>(data: T, options?: PublishOptions): Promise<boolean> {\n if (!this.transport || !this.credentials) {\n throw new StatementConnectionError(\"Not connected. Call connect() first.\");\n }\n\n const dataBytes = encodeData(data);\n const ttl = options?.ttlSeconds ?? this.config.defaultTtlSeconds;\n const expirationTimestamp = Math.floor(Date.now() / 1000) + ttl;\n const sequenceNumber = (Date.now() + this.sequenceCounter++) % 0xffffffff;\n const expiry = createExpiry(expirationTimestamp, sequenceNumber);\n\n const topics: `0x${string}`[] = [this.appTopicHex as `0x${string}`];\n if (options?.topic2) {\n topics.push(topicToHex(createTopic(options.topic2)) as `0x${string}`);\n }\n\n const statement: Statement = {\n expiry,\n topics,\n channel: options?.channel\n ? (topicToHex(createChannel(options.channel)) as `0x${string}`)\n : undefined,\n decryptionKey: options?.decryptionKey\n ? (topicToHex(options.decryptionKey) as `0x${string}`)\n : undefined,\n data: dataBytes,\n };\n\n try {\n await this.transport.signAndSubmit(statement, this.credentials);\n log.debug(\"Published\", { channel: options?.channel });\n return true;\n } catch (error) {\n log.error(\"Publish failed\", {\n error: error instanceof Error ? error.message : String(error),\n });\n return false;\n }\n }\n\n /**\n * Subscribe to incoming statements on this application's topic.\n *\n * @typeParam T - The expected data type (decoded from JSON).\n * @param callback - Called for each new statement.\n * @param options - Optional secondary topic filter.\n * @returns A handle to unsubscribe.\n */\n subscribe<T>(\n callback: (statement: ReceivedStatement<T>) => void,\n options?: { topic2?: string },\n ): Unsubscribable {\n const topic2Hex = options?.topic2 ? topicToHex(createTopic(options.topic2)) : undefined;\n\n const wrappedCallback = (statement: ReceivedStatement<unknown>) => {\n if (topic2Hex) {\n if (!statement.topics[1] || statement.topics[1] !== topic2Hex) return;\n }\n callback(statement as ReceivedStatement<T>);\n };\n\n this.callbacks.push(wrappedCallback);\n\n return {\n unsubscribe: () => {\n const index = this.callbacks.indexOf(wrappedCallback);\n if (index >= 0) {\n this.callbacks.splice(index, 1);\n }\n },\n };\n }\n\n /** Whether the client is connected and ready to publish/subscribe. */\n isConnected(): boolean {\n return this.connected;\n }\n\n /**\n * Get the signer's public key as a hex string (with 0x prefix).\n *\n * @returns The hex-encoded public key, or empty string if not connected or in host mode.\n */\n getPublicKeyHex(): string {\n if (this.credentials?.mode === \"local\") {\n return topicToHex(this.credentials.signer.publicKey);\n }\n return \"\";\n }\n\n /**\n * Destroy the client, unsubscribing and closing the transport.\n *\n * Safe to call multiple times. After destruction, the client cannot be reused.\n */\n destroy(): void {\n // Signal to any in-flight doConnect() that cleanup should happen on its side.\n this.destroyed = true;\n\n if (this.subscription) {\n this.subscription.unsubscribe();\n this.subscription = null;\n }\n\n if (this.transport) {\n this.transport.destroy();\n this.transport = null;\n }\n\n this.credentials = null;\n this.connected = false;\n this.connectPromise = null;\n this.callbacks = [];\n this.seen.clear();\n\n log.info(\"Destroyed\");\n }\n\n // ========================================================================\n // Internal\n // ========================================================================\n\n /* @integration */\n private startSubscription(): void {\n if (!this.transport) return;\n\n const filter = this.buildFilter();\n\n this.subscription = this.transport.subscribe(\n filter,\n (statements) => {\n for (const stmt of statements) {\n this.handleStatementReceived(stmt);\n }\n },\n (error) => {\n log.warn(\"Subscription error\", {\n error: error.message,\n });\n },\n );\n }\n\n /** Remove entries from the seen map whose expiry timestamp is in the past. */\n private pruneSeenMap(): void {\n const nowSeconds = BigInt(Math.floor(Date.now() / 1000));\n for (const [key, expiry] of this.seen) {\n const expiryTimestamp = expiry >> 32n;\n if (expiryTimestamp > 0n && expiryTimestamp < nowSeconds) {\n this.seen.delete(key);\n }\n }\n }\n\n /**\n * Process a received statement, dedup, parse, and deliver to callbacks.\n * Returns true if the statement was new and delivered.\n */\n private handleStatementReceived(stmt: Statement): boolean {\n this.pruneSeenMap();\n\n const parsed = this.parseStatement<unknown>(stmt);\n if (!parsed) return false;\n\n // Deduplication key: channel hex (if present) or blake2b hash of data\n const dedupeKey =\n parsed.channelHex ?? (parsed.raw.data ? toHex(blake2b256(parsed.raw.data)) : \"\");\n\n const existingExpiry = this.seen.get(dedupeKey);\n const newExpiry = parsed.expiry ?? 0n;\n\n if (existingExpiry !== undefined && newExpiry <= existingExpiry) {\n return false;\n }\n\n this.seen.set(dedupeKey, newExpiry);\n\n for (const callback of [...this.callbacks]) {\n try {\n callback(parsed);\n } catch (error) {\n log.error(\"Callback error\", {\n error: error instanceof Error ? error.message : String(error),\n });\n }\n }\n\n return true;\n }\n\n private parseStatement<T>(stmt: Statement): ReceivedStatement<T> | null {\n try {\n if (!stmt.data) return null;\n\n const data = decodeData<T>(stmt.data);\n\n // Extract signer from proof if present\n let signerHex: string | undefined;\n if (stmt.proof) {\n const proofValue = stmt.proof.value as Record<string, unknown>;\n if (\"signer\" in proofValue && typeof proofValue.signer === \"string\") {\n signerHex = proofValue.signer;\n }\n }\n\n return {\n data,\n signerHex,\n channelHex: stmt.channel,\n topics: stmt.topics ?? [],\n expiry: stmt.expiry,\n raw: stmt,\n };\n } catch {\n return null;\n }\n }\n\n /** Build an SdkTopicFilter for the app's primary topic. */\n private buildFilter(topic2Name?: string): SdkTopicFilter {\n const topics = [createTopic(this.config.appName)];\n if (topic2Name) topics.push(createTopic(topic2Name));\n return serializeTopicFilter({ matchAll: topics });\n }\n}\n","// Copyright 2026 Parity Technologies (UK) Ltd.\n// SPDX-License-Identifier: Apache-2.0\nimport { createLogger } from \"@parity/product-sdk-logger\";\n\nimport type { StatementStoreClient } from \"./client.js\";\nimport { createChannel, topicToHex } from \"./topics.js\";\nimport type { PublishOptions, ReceivedStatement, Unsubscribable } from \"./types.js\";\n\nconst log = createLogger(\"statement-store:channels\");\n\n/**\n * Higher-level abstraction providing last-write-wins channel semantics\n * over the statement store.\n *\n * Each channel name maps to a single value. When a new value is written\n * to a channel, it replaces the previous one if its timestamp is newer.\n * This is ideal for presence announcements, signaling, and ephemeral state.\n *\n * @typeParam T - The type of values stored in channels.\n * Values should include a `timestamp` field for ordering;\n * if omitted, the current time is used automatically.\n *\n * @example\n * ```ts\n * interface Presence {\n * type: \"presence\";\n * peerId: string;\n * timestamp: number;\n * }\n *\n * const channels = new ChannelStore<Presence>(client, { topic2: \"doc-123\" });\n *\n * // Write presence\n * await channels.write(\"presence/peer-abc\", {\n * type: \"presence\",\n * peerId: \"abc\",\n * timestamp: Date.now(),\n * });\n *\n * // Read all presences\n * for (const [name, value] of channels.readAll()) {\n * console.log(`${name}: ${value.peerId}`);\n * }\n *\n * // React to changes\n * channels.onChange((name, value, previous) => {\n * console.log(`Channel ${name} updated`);\n * });\n *\n * channels.destroy();\n * ```\n */\nexport class ChannelStore<T extends { timestamp?: number }> {\n private readonly client: StatementStoreClient;\n private readonly topic2: string | undefined;\n private readonly values = new Map<string, T>();\n /** Maps human-readable channel names to their hex hash keys, for consistent lookup. */\n private readonly nameToHash = new Map<string, string>();\n private readonly changeCallbacks: Array<\n (channelName: string, value: T, previous: T | undefined) => void\n > = [];\n private subscription: Unsubscribable | null = null;\n\n /**\n * @param client - The connected {@link StatementStoreClient} to use.\n * @param options - Optional secondary topic for scoping channels.\n */\n constructor(client: StatementStoreClient, options?: { topic2?: string }) {\n this.client = client;\n this.topic2 = options?.topic2;\n\n // Subscribe to incoming statements\n this.subscription = this.client.subscribe<T>(\n (statement) => this.handleStatement(statement),\n { topic2: this.topic2 },\n );\n }\n\n /**\n * Write a value to a named channel.\n *\n * If the value doesn't include a `timestamp`, one is added automatically\n * using `Date.now()`. The value is published to the statement store\n * with the channel name as the statement channel.\n *\n * @param channelName - The channel name (e.g., \"presence/peer-abc\").\n * @param value - The value to write.\n * @returns `true` if the statement was accepted by the network.\n */\n async write(channelName: string, value: T): Promise<boolean> {\n const timestamped = value.timestamp != null ? value : { ...value, timestamp: Date.now() };\n\n const options: PublishOptions = {\n channel: channelName,\n topic2: this.topic2,\n };\n\n const success = await this.client.publish(timestamped, options);\n\n if (success) {\n // Store by hash key so local writes and network echoes use the same key\n const hashKey = topicToHex(createChannel(channelName));\n this.nameToHash.set(channelName, hashKey);\n this.updateChannel(hashKey, timestamped);\n }\n\n return success;\n }\n\n /**\n * Read the latest value for a channel by its human-readable name.\n *\n * Looks up the channel hash from the name, then retrieves the value.\n * Also checks the hash directly for values received from the network\n * before any local write established the name mapping.\n *\n * @param channelName - The channel name (e.g., \"presence/peer-abc\").\n * @returns The latest value, or `undefined` if no value has been received.\n */\n read(channelName: string): T | undefined {\n const hashKey = this.nameToHash.get(channelName) ?? topicToHex(createChannel(channelName));\n return this.values.get(hashKey);\n }\n\n /**\n * Read all channel values as a read-only map.\n *\n * Keys are hex-encoded channel hashes. Use {@link read} for\n * lookup by human-readable name.\n *\n * @returns A map of channel hash to latest value.\n */\n readAll(): ReadonlyMap<string, T> {\n return this.values;\n }\n\n /**\n * Get the number of channels currently tracked.\n */\n get size(): number {\n return this.values.size;\n }\n\n /**\n * Subscribe to channel value changes.\n *\n * The callback is invoked whenever a channel value is updated\n * (either from the network or from a local write).\n *\n * @param callback - Called with the channel key (hex hash for network-received, hex hash for local writes), new value, and previous value.\n * @returns A handle to unsubscribe.\n */\n onChange(\n callback: (channelName: string, value: T, previous: T | undefined) => void,\n ): Unsubscribable {\n this.changeCallbacks.push(callback);\n return {\n unsubscribe: () => {\n const index = this.changeCallbacks.indexOf(callback);\n if (index >= 0) {\n this.changeCallbacks.splice(index, 1);\n }\n },\n };\n }\n\n /**\n * Destroy the channel store and clean up subscriptions.\n *\n * Does not destroy the underlying client.\n */\n destroy(): void {\n if (this.subscription) {\n this.subscription.unsubscribe();\n this.subscription = null;\n }\n this.values.clear();\n this.nameToHash.clear();\n this.changeCallbacks.length = 0;\n log.debug(\"ChannelStore destroyed\");\n }\n\n // ========================================================================\n // Internal\n // ========================================================================\n\n private handleStatement(statement: ReceivedStatement<T>): void {\n // We need a channel to determine the channel name.\n // Without a channel, we can't do last-write-wins deduplication.\n if (!statement.channelHex) return;\n\n // The channel hex is the blake2b of the channel name.\n // We store by the hex representation since we don't have\n // the original channel name from incoming statements.\n this.updateChannel(statement.channelHex, statement.data);\n }\n\n private updateChannel(channelName: string, value: T): void {\n const existing = this.values.get(channelName);\n\n // Last-write-wins: only update if newer\n if (existing) {\n const existingTs = existing.timestamp ?? 0;\n const newTs = value.timestamp ?? 0;\n if (newTs <= existingTs) {\n return; // Same or older value, skip (idempotent)\n }\n }\n\n const previous = existing;\n this.values.set(channelName, value);\n\n // Fire change callbacks (snapshot to handle mid-iteration unsubscribes)\n for (const callback of [...this.changeCallbacks]) {\n try {\n callback(channelName, value, previous);\n } catch (error) {\n log.error(\"onChange callback error\", {\n error: error instanceof Error ? error.message : String(error),\n });\n }\n }\n }\n}\n\nif (import.meta.vitest) {\n const { describe, test, expect, vi, beforeEach } = import.meta.vitest;\n const { configure } = await import(\"@parity/product-sdk-logger\");\n\n beforeEach(() => {\n configure({ handler: () => {} });\n });\n\n // Minimal mock of StatementStoreClient\n function createMockClient() {\n const subscribeCallbacks: Array<(stmt: ReceivedStatement<unknown>) => void> = [];\n\n return {\n subscribe: vi.fn(\n <T>(\n callback: (stmt: ReceivedStatement<T>) => void,\n _options?: { topic2?: string },\n ) => {\n subscribeCallbacks.push(callback as (stmt: ReceivedStatement<unknown>) => void);\n return {\n unsubscribe: () => {\n const idx = subscribeCallbacks.indexOf(\n callback as (stmt: ReceivedStatement<unknown>) => void,\n );\n if (idx >= 0) subscribeCallbacks.splice(idx, 1);\n },\n };\n },\n ),\n publish: vi.fn(async () => true),\n // Helper to simulate incoming statement\n _simulateStatement(stmt: ReceivedStatement<unknown>) {\n for (const cb of subscribeCallbacks) {\n cb(stmt);\n }\n },\n };\n }\n\n type TestValue = { type: string; timestamp: number };\n\n describe(\"ChannelStore\", () => {\n test(\"write publishes and updates local state\", async () => {\n const mockClient = createMockClient();\n const store = new ChannelStore<TestValue>(\n mockClient as unknown as StatementStoreClient,\n );\n\n await store.write(\"presence/abc\", { type: \"presence\", timestamp: 1000 });\n\n expect(mockClient.publish).toHaveBeenCalledOnce();\n // read by name resolves through the hash\n expect(store.read(\"presence/abc\")).toEqual({ type: \"presence\", timestamp: 1000 });\n });\n\n test(\"write adds timestamp if missing\", async () => {\n const mockClient = createMockClient();\n const store = new ChannelStore<{ type: string; timestamp?: number }>(\n mockClient as unknown as StatementStoreClient,\n );\n\n const before = Date.now();\n await store.write(\"ch\", { type: \"test\" });\n\n const calls = mockClient.publish.mock.calls as unknown[][];\n const published = calls[0][0] as { timestamp: number };\n expect(published.timestamp).toBeGreaterThanOrEqual(before);\n });\n\n test(\"write returns false on publish failure\", async () => {\n const mockClient = createMockClient();\n mockClient.publish = vi.fn(async () => false);\n const store = new ChannelStore<TestValue>(\n mockClient as unknown as StatementStoreClient,\n );\n\n const result = await store.write(\"ch\", { type: \"test\", timestamp: 1 });\n expect(result).toBe(false);\n expect(store.read(\"ch\")).toBeUndefined(); // Not stored on failure\n });\n\n test(\"read returns undefined for unknown channel\", () => {\n const mockClient = createMockClient();\n const store = new ChannelStore<TestValue>(\n mockClient as unknown as StatementStoreClient,\n );\n\n expect(store.read(\"unknown\")).toBeUndefined();\n });\n\n test(\"readAll returns all values keyed by hash\", async () => {\n const mockClient = createMockClient();\n const store = new ChannelStore<TestValue>(\n mockClient as unknown as StatementStoreClient,\n );\n\n await store.write(\"a\", { type: \"a\", timestamp: 1 });\n await store.write(\"b\", { type: \"b\", timestamp: 2 });\n\n const all = store.readAll();\n expect(all.size).toBe(2);\n });\n\n test(\"size returns channel count\", async () => {\n const mockClient = createMockClient();\n const store = new ChannelStore<TestValue>(\n mockClient as unknown as StatementStoreClient,\n );\n\n expect(store.size).toBe(0);\n await store.write(\"a\", { type: \"a\", timestamp: 1 });\n expect(store.size).toBe(1);\n await store.write(\"b\", { type: \"b\", timestamp: 2 });\n expect(store.size).toBe(2);\n });\n\n test(\"last-write-wins: newer timestamp replaces older\", async () => {\n const mockClient = createMockClient();\n const store = new ChannelStore<TestValue>(\n mockClient as unknown as StatementStoreClient,\n );\n\n await store.write(\"ch\", { type: \"old\", timestamp: 100 });\n await store.write(\"ch\", { type: \"new\", timestamp: 200 });\n\n expect(store.read(\"ch\")?.type).toBe(\"new\");\n });\n\n test(\"last-write-wins: older timestamp is ignored\", async () => {\n const mockClient = createMockClient();\n const store = new ChannelStore<TestValue>(\n mockClient as unknown as StatementStoreClient,\n );\n\n await store.write(\"ch\", { type: \"new\", timestamp: 200 });\n await store.write(\"ch\", { type: \"old\", timestamp: 100 });\n\n expect(store.read(\"ch\")?.type).toBe(\"new\");\n });\n\n test(\"onChange fires on update with previous value\", async () => {\n const mockClient = createMockClient();\n const store = new ChannelStore<TestValue>(\n mockClient as unknown as StatementStoreClient,\n );\n const onChange = vi.fn();\n store.onChange(onChange);\n\n await store.write(\"ch\", { type: \"first\", timestamp: 1 });\n expect(onChange).toHaveBeenCalledOnce();\n expect(onChange.mock.calls[0][1]).toEqual({ type: \"first\", timestamp: 1 });\n expect(onChange.mock.calls[0][2]).toBeUndefined();\n\n await store.write(\"ch\", { type: \"second\", timestamp: 2 });\n expect(onChange).toHaveBeenCalledTimes(2);\n expect(onChange.mock.calls[1][1]).toEqual({ type: \"second\", timestamp: 2 });\n expect(onChange.mock.calls[1][2]).toEqual({ type: \"first\", timestamp: 1 });\n });\n\n test(\"onChange unsubscribe stops notifications\", async () => {\n const mockClient = createMockClient();\n const store = new ChannelStore<TestValue>(\n mockClient as unknown as StatementStoreClient,\n );\n const onChange = vi.fn();\n const sub = store.onChange(onChange);\n\n await store.write(\"ch\", { type: \"first\", timestamp: 1 });\n expect(onChange).toHaveBeenCalledOnce();\n\n sub.unsubscribe();\n\n await store.write(\"ch\", { type: \"second\", timestamp: 2 });\n expect(onChange).toHaveBeenCalledOnce(); // No additional call\n });\n\n test(\"destroy cleans up\", async () => {\n const mockClient = createMockClient();\n const store = new ChannelStore<TestValue>(\n mockClient as unknown as StatementStoreClient,\n );\n\n await store.write(\"ch\", { type: \"test\", timestamp: 1 });\n store.destroy();\n\n expect(store.readAll().size).toBe(0);\n });\n\n test(\"destroy is safe when subscription is already null\", () => {\n const mockClient = createMockClient();\n const store = new ChannelStore<TestValue>(\n mockClient as unknown as StatementStoreClient,\n );\n store.destroy();\n // Second destroy — subscription is already null\n expect(() => store.destroy()).not.toThrow();\n });\n });\n}\n"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@parity/product-sdk-statement-store",
3
3
  "description": "Publish/subscribe client for the Polkadot Statement Store with host-first transport and topic-based routing",
4
- "version": "0.4.10",
4
+ "version": "0.5.0",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
@@ -19,23 +19,13 @@
19
19
  "access": "public"
20
20
  },
21
21
  "dependencies": {
22
- "@novasamatech/sdk-statement": "^0.6.0",
23
22
  "@polkadot-api/substrate-client": "^0.7.0",
24
23
  "polkadot-api": "^2.1.6",
25
- "@parity/product-sdk-host": "0.11.0",
26
- "@parity/product-sdk-logger": "0.1.1",
27
- "@parity/product-sdk-utils": "0.1.1"
28
- },
29
- "peerDependencies": {
30
- "@novasamatech/host-api-wrapper": ">=0.8.0"
31
- },
32
- "peerDependenciesMeta": {
33
- "@novasamatech/host-api-wrapper": {
34
- "optional": true
35
- }
24
+ "@parity/product-sdk-host": "0.12.0",
25
+ "@parity/product-sdk-utils": "0.1.1",
26
+ "@parity/product-sdk-logger": "0.1.1"
36
27
  },
37
28
  "devDependencies": {
38
- "@novasamatech/host-api-wrapper": "^0.8.9",
39
29
  "tsup": "^8.5.1",
40
30
  "typescript": "^5.9.3",
41
31
  "vitest": "^3.1.4"