Polkadot Apps
    Preparing search index...

    Module @polkadot-apps/statement-store

    @polkadot-apps/statement-store

    Publish/subscribe client for the Polkadot Statement Store with host-first transport and topic-based routing.

    pnpm add @polkadot-apps/statement-store
    

    This package depends on @polkadot-apps/host, @polkadot-apps/logger, @polkadot-apps/utils, @novasamatech/sdk-statement, and @polkadot-api/substrate-client, which are installed automatically. The optional peer dependency @novasamatech/product-sdk is required for host mode (inside containers).

    The client supports two connection modes depending on the runtime environment.

    Inside Polkadot Desktop/Mobile, proof creation and submission are delegated to the host API. No WebSocket endpoint is needed.

    import { StatementStoreClient } from "@polkadot-apps/statement-store";

    const client = new StatementStoreClient({ appName: "my-app" });
    await client.connect({ mode: "host", accountId: ["5Grw...", 42] });

    await client.publish({ type: "hello", peerId: "abc" }, {
    channel: "presence/abc",
    topic2: "room-123",
    });

    const sub = client.subscribe<{ type: string }>(statement => {
    console.log(statement.data.type);
    });

    sub.unsubscribe();
    client.destroy();

    Outside containers, statements are signed locally with an Sr25519 signer and submitted over WebSocket RPC.

    import { StatementStoreClient } from "@polkadot-apps/statement-store";

    const signer = {
    publicKey: myPublicKey, // Uint8Array, 32 bytes
    sign: (msg) => mySignFn(msg), // Returns Uint8Array (64 bytes) or Promise<Uint8Array>
    };

    const client = new StatementStoreClient({
    appName: "my-app",
    endpoint: "wss://paseo-bulletin-rpc.polkadot.io",
    });
    await client.connect({ mode: "local", signer });

    await client.publish({ type: "hello", peerId: "abc" }, {
    channel: "presence/abc",
    });

    client.destroy();

    The primary interface for publishing and subscribing to statements. Handles JSON encoding, signing (host or local), topic management, and resilient delivery via subscription with polling fallback.

    import { StatementStoreClient } from "@polkadot-apps/statement-store";

    const client = new StatementStoreClient({
    appName: "my-app", // Required. Used as primary topic (blake2b hash).
    endpoint: "wss://rpc.example.com", // Optional. Fallback WebSocket endpoint.
    pollIntervalMs: 10_000, // Optional. Polling interval. Default: 10000.
    defaultTtlSeconds: 30, // Optional. Statement TTL. Default: 30.
    enablePolling: true, // Optional. Enable polling fallback. Default: true.
    transport: customTransport, // Optional. BYOD transport, skips auto-detection.
    });

    Call connect with credentials matching the runtime environment. The transport is resolved automatically: host API first, then direct WebSocket RPC as fallback.

    // Host mode — inside a container
    await client.connect({ mode: "host", accountId: ["5Grw...", 42] });

    // Local mode — outside a container
    await client.connect({ mode: "local", signer: { publicKey, sign } });

    console.log(client.isConnected()); // true
    console.log(client.getPublicKeyHex()); // "0xaa..." (local mode only)

    The legacy signature connect(signer) is still supported for backward compatibility but deprecated in favor of connect({ mode: "local", signer }).

    Publish typed JSON data. Returns true if the network accepted the statement, false if rejected or errored.

    const accepted = await client.publish(
    { type: "presence", peerId: "abc", timestamp: Date.now() },
    {
    channel: "presence/abc", // Optional. Enables last-write-wins deduplication.
    topic2: "room-123", // Optional. Secondary topic for subscriber filtering.
    ttlSeconds: 60, // Optional. Overrides default TTL.
    decryptionKey: keyBytes, // Optional. 32-byte hint for filtering.
    },
    );

    Data is serialized as JSON and encoded to UTF-8. The maximum payload size is 512 bytes.

    Listen for incoming statements in real time. Statements are deduplicated by channel and expiry.

    const sub = client.subscribe<{ type: string; peerId: string }>(
    (statement) => {
    console.log(statement.data.type);
    console.log(statement.signerHex); // string | undefined
    console.log(statement.channelHex); // string | undefined
    console.log(statement.topics); // string[]
    console.log(statement.expiry); // bigint | undefined
    },
    { topic2: "room-123" }, // Optional. Filter by secondary topic.
    );

    // Stop listening
    sub.unsubscribe();

    Fetch statements that were published before the subscription started. Only available in RPC mode (local). In host mode, the subscription replays existing statements automatically.

    const statements = await client.query<{ type: string }>({
    topic2: "room-123",
    });

    for (const stmt of statements) {
    console.log(stmt.data, stmt.signerHex);
    }
    client.destroy(); // Stops polling, unsubscribes, closes transport. Safe to call multiple times.
    

    A higher-level abstraction providing last-write-wins semantics over StatementStoreClient. Each named channel holds a single value; newer writes replace older ones by timestamp.

    import { ChannelStore } from "@polkadot-apps/statement-store";

    interface Presence {
    type: "presence";
    peerId: string;
    timestamp: number;
    }

    const channels = new ChannelStore<Presence>(client, { topic2: "doc-123" });

    // Write
    await channels.write("presence/peer-abc", {
    type: "presence",
    peerId: "abc",
    timestamp: Date.now(),
    });

    // Read a single channel
    const value = channels.read("presence/peer-abc"); // Presence | undefined

    // Read all channels
    for (const [hashKey, value] of channels.readAll()) {
    console.log(value.peerId);
    }

    // Track the number of active channels
    console.log(channels.size);

    // React to changes
    const sub = channels.onChange((channelKey, value, previous) => {
    console.log(`Updated: ${channelKey}`, value, previous);
    });

    sub.unsubscribe();
    channels.destroy();

    If the written value lacks a timestamp field, one is added automatically using Date.now().

    import {
    createTopic,
    createChannel,
    topicToHex,
    topicsEqual,
    serializeTopicFilter,
    } from "@polkadot-apps/statement-store";

    const topic = createTopic("my-app"); // TopicHash (blake2b-256)
    const channel = createChannel("presence"); // ChannelHash (blake2b-256)

    const hex = topicToHex(topic); // "0x..."
    const equal = topicsEqual(topicA, topicB); // boolean

    const serialized = serializeTopicFilter({ matchAll: [topic] });
    // { matchAll: ["0x..."] }
    Constant Value Description
    MAX_STATEMENT_SIZE 512 Maximum data payload size in bytes
    MAX_USER_TOTAL 1024 Maximum total storage per user in bytes
    DEFAULT_TTL_SECONDS 30 Default statement time-to-live in seconds
    DEFAULT_POLL_INTERVAL_MS 10000 Default polling interval in milliseconds

    All errors extend StatementStoreError. Catch the base class to handle any error from this package.

    import {
    StatementStoreError,
    StatementEncodingError,
    StatementSubmitError,
    StatementSubscriptionError,
    StatementConnectionError,
    StatementDataTooLargeError,
    } from "@polkadot-apps/statement-store";

    try {
    await client.publish(data);
    } catch (err) {
    if (err instanceof StatementDataTooLargeError) {
    console.error(`Too large: ${err.actualSize}/${err.maxSize} bytes`);
    } else if (err instanceof StatementConnectionError) {
    console.error("Not connected");
    } else if (err instanceof StatementStoreError) {
    console.error("Statement store error:", err.message);
    }
    }
    Error class When it is thrown Extra properties
    StatementEncodingError JSON encode/decode failed --
    StatementSubmitError Node rejected the statement detail: unknown
    StatementSubscriptionError Subscription setup failed (non-fatal) --
    StatementConnectionError Transport connection failed --
    StatementDataTooLargeError Data exceeds 512 bytes actualSize: number, maxSize: number
    class StatementStoreClient {
    constructor(config: StatementStoreConfig)
    connect(credentials: ConnectionCredentials): Promise<void>
    connect(signer: StatementSignerWithKey): Promise<void> // deprecated
    publish<T>(data: T, options?: PublishOptions): Promise<boolean>
    subscribe<T>(callback: (statement: ReceivedStatement<T>) => void, options?: { topic2?: string }): Unsubscribable
    query<T>(options?: { topic2?: string }): Promise<ReceivedStatement<T>[]>
    isConnected(): boolean
    getPublicKeyHex(): string
    destroy(): void
    }
    class ChannelStore<T extends { timestamp?: number }> {
    constructor(client: StatementStoreClient, options?: { topic2?: string })
    write(channelName: string, value: T): Promise<boolean>
    read(channelName: string): T | undefined
    readAll(): ReadonlyMap<string, T>
    get size(): number
    onChange(callback: (channelName: string, value: T, previous: T | undefined) => void): Unsubscribable
    destroy(): void
    }
    function createTopic(name: string): TopicHash
    function createChannel(name: string): ChannelHash
    function topicToHex(hash: Uint8Array): string
    function topicsEqual(a: Uint8Array, b: Uint8Array): boolean
    function serializeTopicFilter(filter: TopicFilter): SdkTopicFilter
    function createTransport(config: { endpoint?: string }): Promise<StatementTransport>
    

    The createTransport factory tries the Host API first (inside containers), then falls back to direct WebSocket RPC via @polkadot-api/substrate-client + @novasamatech/sdk-statement. Most consumers should use StatementStoreClient instead of calling this directly.

    /** Connection credentials — host mode or local mode. */
    type ConnectionCredentials =
    | { mode: "host"; accountId: [string, number] }
    | { mode: "local"; signer: StatementSignerWithKey };

    interface StatementStoreConfig {
    appName: string;
    endpoint?: string;
    pollIntervalMs?: number; // Default: 10000
    defaultTtlSeconds?: number; // Default: 30
    enablePolling?: boolean; // Default: true
    transport?: StatementTransport; // BYOD
    }

    interface PublishOptions {
    channel?: string;
    topic2?: string;
    ttlSeconds?: number;
    decryptionKey?: Uint8Array;
    }

    interface ReceivedStatement<T = unknown> {
    data: T;
    signerHex?: string;
    channelHex?: string;
    topics: string[];
    expiry?: bigint;
    raw: Statement;
    }

    interface StatementSignerWithKey {
    publicKey: Uint8Array;
    sign: (message: Uint8Array) => Uint8Array | Promise<Uint8Array>;
    }

    /** Branded 32-byte blake2b-256 hash for statement topics. */
    type TopicHash = Uint8Array & { readonly __brand: "TopicHash" };

    /** Branded 32-byte blake2b-256 hash for statement channels. */
    type ChannelHash = Uint8Array & { readonly __brand: "ChannelHash" };

    type TopicFilter = "any" | { matchAll: TopicHash[] } | { matchAny: TopicHash[] };

    interface StatementTransport {
    subscribe(filter: SdkTopicFilter, onStatements: (statements: Statement[]) => void, onError: (error: Error) => void): Unsubscribable;
    signAndSubmit(statement: Statement, credentials: ConnectionCredentials): Promise<void>;
    query?(filter: SdkTopicFilter): Promise<Statement[]>;
    destroy(): void;
    }

    interface Unsubscribable {
    unsubscribe: () => void;
    }

    Apache-2.0

    Classes

    ChannelStore
    StatementConnectionError
    StatementDataTooLargeError
    StatementEncodingError
    StatementStoreClient
    StatementStoreError
    StatementSubmitError
    StatementSubscriptionError

    Interfaces

    PublishOptions
    ReceivedStatement
    StatementSignerWithKey
    StatementStoreConfig
    StatementTransport
    Unsubscribable

    Type Aliases

    ChannelHash
    ConnectionCredentials
    Proof
    SdkTopicFilter
    SignedStatement
    Statement
    StatementSigner
    SubmitResult
    TopicFilter
    TopicHash
    UnsignedStatement

    Variables

    DEFAULT_POLL_INTERVAL_MS
    DEFAULT_TTL_SECONDS
    MAX_STATEMENT_SIZE
    MAX_USER_TOTAL

    Functions

    createChannel
    createTopic
    createTransport
    decodeData
    encodeData
    fromHex
    serializeTopicFilter
    toHex
    topicsEqual
    topicToHex