@novasamatech/statement-store 0.8.7-0 → 0.8.7-2

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.
@@ -0,0 +1,32 @@
1
+ import type { Statement } from '@novasamatech/sdk-statement';
2
+ import type { StatementStoreAdapter } from './types.js';
3
+ /**
4
+ * An in-memory {@link StatementStoreAdapter} that replicates the real statement
5
+ * store's observable contract, for tests and local development.
6
+ *
7
+ * Fidelity rules (mirroring the on-chain store):
8
+ * - One statement per channel. A same-channel write is accepted only with a
9
+ * STRICTLY HIGHER expiry; an equal-or-lower expiry is rejected with
10
+ * {@link ExpiryTooLowError} (the store's `channelPriorityTooLow`). A
11
+ * byte-identical resubmission is "known" → ok, with no duplicate.
12
+ * - `queryStatements` returns the current per-channel statements matching the
13
+ * topic filter (superseded statements are evicted).
14
+ * - `subscribeStatements` streams statements submitted AFTER subscription that
15
+ * match the filter — the initial snapshot is obtained via `queryStatements`,
16
+ * exactly as a consumer does. Delivery is synchronous on submit.
17
+ *
18
+ * Two mirrored sessions can share ONE store to exercise full bidirectional
19
+ * host ↔ peer flows: a submit by one is observed by the other.
20
+ *
21
+ * It IS a {@link StatementStoreAdapter} (pass it straight to `createSession`)
22
+ * with extra inspection helpers attached.
23
+ */
24
+ export type InMemoryStatementStore = StatementStoreAdapter & {
25
+ /** Statements currently retained — one per channel (highest expiry wins). */
26
+ currentStatements(): Statement[];
27
+ /** Every accepted submission, in order (excludes rejected writes and known no-ops). */
28
+ acceptedStatements(): Statement[];
29
+ /** Number of live subscriptions (increases on subscribe, decreases on unsubscribe). */
30
+ activeSubscriptions(): number;
31
+ };
32
+ export declare function createInMemoryStatementStore(): InMemoryStatementStore;
@@ -0,0 +1,56 @@
1
+ import { errAsync, okAsync } from 'neverthrow';
2
+ import { toHex } from 'polkadot-api/utils';
3
+ import { ExpiryTooLowError } from './types.js';
4
+ export function createInMemoryStatementStore() {
5
+ const channels = new Map();
6
+ const accepted = [];
7
+ const subscribers = new Set();
8
+ const topicsOf = (s) => s.topics ?? [];
9
+ const matches = (filter, s) => {
10
+ const topics = topicsOf(s);
11
+ return 'matchAll' in filter
12
+ ? filter.matchAll.every(t => topics.includes(toHex(t)))
13
+ : filter.matchAny.some(t => topics.includes(toHex(t)));
14
+ };
15
+ // Statement identity for the "known" (dedup) check.
16
+ const keyOf = (s) => `${s.channel ?? ''}|${(s.expiry ?? 0n).toString()}|${topicsOf(s).join(',')}|${s.data ? toHex(s.data) : ''}`;
17
+ const adapter = {
18
+ queryStatements(filter) {
19
+ return okAsync([...channels.values()].filter(s => matches(filter, s)));
20
+ },
21
+ subscribeStatements(filter, callback) {
22
+ const sub = { filter, callback };
23
+ subscribers.add(sub);
24
+ return () => {
25
+ subscribers.delete(sub);
26
+ };
27
+ },
28
+ submitStatement(statement) {
29
+ const channel = statement.channel ?? '';
30
+ const existing = channels.get(channel);
31
+ const submittedExpiry = statement.expiry ?? 0n;
32
+ if (existing) {
33
+ if (keyOf(existing) === keyOf(statement))
34
+ return okAsync(undefined); // known
35
+ const existingExpiry = existing.expiry ?? 0n;
36
+ if (submittedExpiry <= existingExpiry) {
37
+ return errAsync(new ExpiryTooLowError(submittedExpiry, existingExpiry));
38
+ }
39
+ }
40
+ channels.set(channel, statement);
41
+ accepted.push(statement);
42
+ for (const sub of subscribers) {
43
+ if (matches(sub.filter, statement)) {
44
+ sub.callback({ statements: [statement], isComplete: true });
45
+ }
46
+ }
47
+ return okAsync(undefined);
48
+ },
49
+ };
50
+ return {
51
+ ...adapter,
52
+ currentStatements: () => [...channels.values()],
53
+ acceptedStatements: () => [...accepted],
54
+ activeSubscriptions: () => subscribers.size,
55
+ };
56
+ }
package/dist/index.d.ts CHANGED
@@ -5,14 +5,17 @@ export type { AccountId, LocalSessionAccount, RemoteSessionAccount, SessionAccou
5
5
  export { AccountIdCodec, LocalSessionAccountCodec, RemoteSessionAccountCodec, createAccountId, createLocalSessionAccount, createRemoteSessionAccount, } from './model/sessionAccount.js';
6
6
  export type { Session } from './session/types.js';
7
7
  export { createSession } from './session/session.js';
8
+ export type { ResponseStatus } from './session/scale/statementData.js';
8
9
  export { Request, Response, ResponseCode, StatementData } from './session/scale/statementData.js';
9
10
  export type { StatementProver } from './session/statementProver.js';
10
- export { createSr25519Prover } from './session/statementProver.js';
11
+ export { createSlotAccountProver, createSr25519Prover } from './session/statementProver.js';
11
12
  export type { Encryption } from './session/encyption.js';
12
13
  export { createEncryption } from './session/encyption.js';
13
14
  export { DecodingError, DecryptionError, UnknownError } from './session/error.js';
14
15
  export type { LazyClient } from './adapter/lazyClient.js';
15
16
  export { createLazyClient } from './adapter/lazyClient.js';
17
+ export type { InMemoryStatementStore } from './adapter/inMemory.js';
18
+ export { createInMemoryStatementStore } from './adapter/inMemory.js';
16
19
  export type { StatementStoreAdapter } from './adapter/types.js';
17
20
  export { AccountFullError, AlreadyExpiredError, BadProofError, DataTooLargeError, EncodingTooLargeError, ExpiryTooLowError, InternalStoreError, KnownExpiredError, NoAllowanceError, NoProofError, StorageFullError, } from './adapter/types.js';
18
21
  export { createPapiStatementStoreAdapter } from './adapter/rpc.js';
package/dist/index.js CHANGED
@@ -2,10 +2,11 @@ export { SessionIdCodec, createSessionId } from './model/session.js';
2
2
  export { AccountIdCodec, LocalSessionAccountCodec, RemoteSessionAccountCodec, createAccountId, createLocalSessionAccount, createRemoteSessionAccount, } from './model/sessionAccount.js';
3
3
  export { createSession } from './session/session.js';
4
4
  export { Request, Response, ResponseCode, StatementData } from './session/scale/statementData.js';
5
- export { createSr25519Prover } from './session/statementProver.js';
5
+ export { createSlotAccountProver, createSr25519Prover } from './session/statementProver.js';
6
6
  export { createEncryption } from './session/encyption.js';
7
7
  export { DecodingError, DecryptionError, UnknownError } from './session/error.js';
8
8
  export { createLazyClient } from './adapter/lazyClient.js';
9
+ export { createInMemoryStatementStore } from './adapter/inMemory.js';
9
10
  export { AccountFullError, AlreadyExpiredError, BadProofError, DataTooLargeError, EncodingTooLargeError, ExpiryTooLowError, InternalStoreError, KnownExpiredError, NoAllowanceError, NoProofError, StorageFullError, } from './adapter/types.js';
10
11
  export { createPapiStatementStoreAdapter } from './adapter/rpc.js';
11
12
  export { createSr25519Derivation, createSr25519Secret, deriveSlotAccountPublicKey, deriveSr25519PublicKey, ensureSubstrateSlotSr25519Ready, ensureSubstrateSr25519Ready, khash, signSlotAccountSecret, signWithSr25519Secret, verifySlotAccountSignature, verifySr25519Signature, } from './crypto.js';
@@ -0,0 +1 @@
1
+ export declare function nextExpiry(current: bigint): bigint;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Statement expiry/priority, u64 = (expiration_epoch << 32) | priority (spec layout).
3
+ * We pin the high word to 0xFFFFFFFF (max → effectively non-expiring, matching iOS &
4
+ * Android) and use the low word as a wall-clock-floored monotonic priority, so channel
5
+ * supersession is driven by the priority regardless of how the store compares the field.
6
+ * Returns a value strictly greater than `current` (i.e. `max(current + 1, now-priority)`).
7
+ */
8
+ const NEVER_EXPIRE_HIGH = 0xffffffffn;
9
+ /**
10
+ * Priority epoch base: seconds at 2025-11-15T00:00:00Z. The low word is a u32 priority counted FROM
11
+ * this epoch (spec §1), not the raw Unix timestamp. iOS (StatementPriorityFactory.unixOffset) and
12
+ * Android subtract the same offset; omitting it makes the TS low word ~1.76e9 larger than every
13
+ * mobile client's, so any cross-client or shared-channel priority comparison would always rank a
14
+ * TS-written statement above a mobile-written one. Keeping the base aligned removes that landmine.
15
+ */
16
+ const PRIORITY_EPOCH_OFFSET = 1763164800n;
17
+ export function nextExpiry(current) {
18
+ const nowSecs = BigInt(Math.floor(Date.now() / 1000));
19
+ const priority = nowSecs > PRIORITY_EPOCH_OFFSET ? nowSecs - PRIORITY_EPOCH_OFFSET : 0n;
20
+ const timestampPriority = (NEVER_EXPIRE_HIGH << 32n) | priority;
21
+ return timestampPriority > current ? timestampPriority : current + 1n;
22
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,43 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { nextExpiry } from './priority.js';
3
+ // Statement expiry/priority: u64 = (expiration_epoch << 32) | priority. The high word is pinned
4
+ // to 0xFFFFFFFF (non-expiring) and the low word is a wall-clock-floored monotonic priority that
5
+ // drives channel supersession. (Spec §1; matches iOS/Android.)
6
+ describe('expiry priority', () => {
7
+ it('encodes a non-expiring statement (high word pinned to 0xFFFFFFFF)', () => {
8
+ expect(nextExpiry(0n) >> 32n).toBe(0xffffffffn);
9
+ });
10
+ it('carries a wall-clock priority in the low word', () => {
11
+ const result = nextExpiry(0n);
12
+ expect(result & 0xffffffffn).toBeGreaterThan(0n);
13
+ });
14
+ it('counts the low word from the 2025-11-15 priority epoch (matches iOS/Android)', () => {
15
+ // iOS StatementPriorityFactory: priority = unixSeconds - 1_763_164_800 (the 2025-11-15 base,
16
+ // spec §1). The TS SDK must use the SAME base; otherwise its low word is ~1.76e9 larger than
17
+ // every mobile client's, so any cross-client/shared-channel priority comparison would rank a
18
+ // TS-written statement above a mobile-written one regardless of real time.
19
+ const PRIORITY_EPOCH_OFFSET = 1763164800n;
20
+ vi.useFakeTimers();
21
+ try {
22
+ const fixedMs = 1_780_000_000_000;
23
+ vi.setSystemTime(fixedMs);
24
+ const expected = BigInt(Math.floor(fixedMs / 1000)) - PRIORITY_EPOCH_OFFSET;
25
+ expect(nextExpiry(0n) & 0xffffffffn).toBe(expected);
26
+ }
27
+ finally {
28
+ vi.useRealTimers();
29
+ }
30
+ });
31
+ it('increments by one when the current value already exceeds the wall-clock priority', () => {
32
+ const high = (0xffffffffn << 32n) | 0xffffffffn; // max u64
33
+ expect(nextExpiry(high)).toBe(high + 1n);
34
+ });
35
+ it('is strictly monotonic across repeated calls', () => {
36
+ let expiry = 0n;
37
+ for (let i = 0; i < 5; i++) {
38
+ const next = nextExpiry(expiry);
39
+ expect(next).toBeGreaterThan(expiry);
40
+ expiry = next;
41
+ }
42
+ });
43
+ });
@@ -25,5 +25,11 @@ export type SessionParams = {
25
25
  sessionKey: Uint8Array;
26
26
  maxRequestSize?: number;
27
27
  };
28
- export declare function nextExpiry(current: bigint): bigint;
28
+ /**
29
+ * Fixed per-statement wire overhead reserved before sizing the request payload:
30
+ * topic (32) + channel (32) + expiry (8) + proof signature (64) + signer (32).
31
+ * Mirrors the Android/iOS sessions, which size message batches against
32
+ * `maxStatementSize - overhead` rather than the raw statement limit.
33
+ */
34
+ export declare const STATEMENT_OVERHEAD: number;
29
35
  export declare function createSession({ localAccount, remoteAccount, statementStore, encryption, prover, sessionKey, maxRequestSize, }: SessionParams): Session;