@novasamatech/statement-store 0.6.18 → 0.7.0-1

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.
@@ -1,6 +1,5 @@
1
1
  import type { RequestFn, SubscribeFn } from '@novasamatech/sdk-statement';
2
- import type { JsonRpcProvider } from '@polkadot-api/json-rpc-provider';
3
- import type { PolkadotClient } from 'polkadot-api';
2
+ import type { JsonRpcProvider, PolkadotClient } from 'polkadot-api';
4
3
  export type LazyClient = ReturnType<typeof createLazyClient>;
5
4
  export declare const createLazyClient: (provider: JsonRpcProvider) => {
6
5
  getClient(): PolkadotClient;
@@ -1,15 +1,19 @@
1
+ import { toHex } from '@novasamatech/scale';
1
2
  import { createStatementSdk } from '@novasamatech/sdk-statement';
2
- import { toHex } from '@polkadot-api/utils';
3
3
  import { errAsync, fromPromise, okAsync } from 'neverthrow';
4
4
  import { toError } from '../helpers.js';
5
5
  import { AccountFullError, AlreadyExpiredError, BadProofError, DataTooLargeError, EncodingTooLargeError, ExpiryTooLowError, InternalStoreError, KnownExpiredError, NoAllowanceError, NoProofError, StorageFullError, } from './types.js';
6
- function createKey(topics) {
7
- return topics.map(toHex).sort().join('');
6
+ function toSdkTopicFilter(filter) {
7
+ if ('matchAll' in filter) {
8
+ return { matchAll: filter.matchAll.map(toHex) };
9
+ }
10
+ return { matchAny: filter.matchAny.map(toHex) };
8
11
  }
9
- function toTopicFilter(topics) {
10
- if (topics.length === 0)
11
- return 'any';
12
- return { matchAll: topics.map(t => toHex(t)) };
12
+ function createKey(filter) {
13
+ if ('matchAll' in filter) {
14
+ return `matchAll:${filter.matchAll.map(toHex).sort().join(',')}`;
15
+ }
16
+ return `matchAny:${filter.matchAny.map(toHex).sort().join(',')}`;
13
17
  }
14
18
  export function createPapiStatementStoreAdapter(lazyClient) {
15
19
  const sdk = createStatementSdk(lazyClient.getRequestFn(), lazyClient.getSubscribeFn());
@@ -25,28 +29,24 @@ export function createPapiStatementStoreAdapter(lazyClient) {
25
29
  return list;
26
30
  }
27
31
  function removeCallback(key, callback) {
28
- let list = callbacks.get(key);
32
+ const list = callbacks.get(key);
29
33
  if (!list)
30
34
  return [];
31
- list = list.filter(x => x !== callback);
32
- if (list.length === 0) {
35
+ const idx = list.indexOf(callback);
36
+ if (idx !== -1)
37
+ list.splice(idx, 1);
38
+ if (list.length === 0)
33
39
  callbacks.delete(key);
34
- }
35
- else {
36
- callbacks.set(key, list);
37
- }
38
40
  return list;
39
41
  }
40
42
  const adapter = {
41
- queryStatements(topics) {
42
- const filter = toTopicFilter(topics);
43
- return fromPromise(sdk.getStatements(filter), toError);
43
+ queryStatements(filter) {
44
+ return fromPromise(sdk.getStatements(toSdkTopicFilter(filter)), toError);
44
45
  },
45
- subscribeStatements(topics, callback) {
46
- const key = createKey(topics);
46
+ subscribeStatements(filter, callback) {
47
+ const key = createKey(filter);
47
48
  const list = addCallback(key, callback);
48
49
  if (list.length === 1) {
49
- const filter = toTopicFilter(topics);
50
50
  let batch = [];
51
51
  let flushScheduled = false;
52
52
  const flush = () => {
@@ -58,11 +58,11 @@ export function createPapiStatementStoreAdapter(lazyClient) {
58
58
  const currentCallbacks = callbacks.get(key);
59
59
  if (currentCallbacks) {
60
60
  for (const fn of currentCallbacks) {
61
- fn(statements);
61
+ fn({ statements, isComplete: true });
62
62
  }
63
63
  }
64
64
  };
65
- const unsub = sdk.subscribeStatements(filter, statement => {
65
+ const unsub = sdk.subscribeStatements(toSdkTopicFilter(filter), statement => {
66
66
  batch.push(statement);
67
67
  if (!flushScheduled) {
68
68
  flushScheduled = true;
@@ -1,8 +1,17 @@
1
1
  import type { SignedStatement, Statement } from '@novasamatech/sdk-statement';
2
2
  import type { ResultAsync } from 'neverthrow';
3
+ export type TopicFilter = {
4
+ matchAll: Uint8Array[];
5
+ } | {
6
+ matchAny: Uint8Array[];
7
+ };
8
+ export type StatementsPage = {
9
+ statements: Statement[];
10
+ isComplete: boolean;
11
+ };
3
12
  export type StatementStoreAdapter = {
4
- queryStatements(topics: Uint8Array[], destination?: Uint8Array): ResultAsync<Statement[], Error>;
5
- subscribeStatements(topics: Uint8Array[], callback: (statements: Statement[]) => unknown): VoidFunction;
13
+ queryStatements(filter: TopicFilter, destination?: Uint8Array): ResultAsync<Statement[], Error>;
14
+ subscribeStatements(filter: TopicFilter, callback: (page: StatementsPage) => unknown): VoidFunction;
6
15
  submitStatement(statement: SignedStatement): ResultAsync<void, DataTooLargeError | ExpiryTooLowError | AccountFullError | StorageFullError | NoProofError | BadProofError | EncodingTooLargeError | NoAllowanceError | AlreadyExpiredError | KnownExpiredError | InternalStoreError | Error>;
7
16
  };
8
17
  export declare class DataTooLargeError extends Error {
package/dist/crypto.d.ts CHANGED
@@ -4,7 +4,7 @@ export declare function stringToBytes(str: string): Uint8Array<ArrayBuffer>;
4
4
  /**
5
5
  * blake2b_256 with key
6
6
  */
7
- export declare function khash(secret: Uint8Array, message: Uint8Array): Uint8Array<ArrayBufferLike>;
7
+ export declare function khash(secret: Uint8Array, message: Uint8Array): Uint8Array<ArrayBufferLike> & Uint8Array<ArrayBuffer>;
8
8
  export declare function createSr25519Secret(entropy: Uint8Array, derivation?: string): Uint8Array<ArrayBufferLike>;
9
9
  export declare function createSr25519Derivation(secret: Uint8Array, derivation: string): Uint8Array<ArrayBufferLike>;
10
10
  export declare function deriveSr25519PublicKey(secret: Uint8Array): Uint8Array;
@@ -1,4 +1,4 @@
1
- import { mergeUint8 } from '@polkadot-api/utils';
1
+ import { mergeUint8 } from 'polkadot-api/utils';
2
2
  import { Bytes } from 'scale-ts';
3
3
  import { khash, stringToBytes } from '../crypto.js';
4
4
  export const SessionIdCodec = Bytes(32);
@@ -2,8 +2,8 @@ import { gcm } from '@noble/ciphers/aes.js';
2
2
  import { hkdf } from '@noble/hashes/hkdf.js';
3
3
  import { sha256 } from '@noble/hashes/sha2.js';
4
4
  import { randomBytes } from '@noble/hashes/utils.js';
5
- import { mergeUint8 } from '@polkadot-api/utils';
6
5
  import { Result, fromThrowable } from 'neverthrow';
6
+ import { mergeUint8 } from 'polkadot-api/utils';
7
7
  export function createEncryption(sharedSecret) {
8
8
  const salt = new Uint8Array(); // secure enough since P256 random keys provide enough entropy
9
9
  const info = new Uint8Array(); // no need to introduce any context
@@ -1,7 +1,7 @@
1
1
  import { createExpiryFromDuration } from '@novasamatech/sdk-statement';
2
- import { toHex } from '@polkadot-api/utils';
3
2
  import { nanoid } from 'nanoid';
4
3
  import { ResultAsync, err, errAsync, fromPromise, fromThrowable, ok, okAsync } from 'neverthrow';
4
+ import { toHex } from 'polkadot-api/utils';
5
5
  import { khash, stringToBytes } from '../crypto.js';
6
6
  import { nonNullable, toError } from '../helpers.js';
7
7
  import { createSessionId } from '../model/session.js';
@@ -150,21 +150,17 @@ export function createSession({ localAccount, remoteAccount, statementStore, enc
150
150
  }
151
151
  }
152
152
  function ensureStoreSubscription() {
153
- if (storeUnsub) {
154
- console.info('[session] ensureStoreSubscription: already subscribed');
153
+ if (storeUnsub)
155
154
  return;
156
- }
157
- console.info('[session] ensureStoreSubscription: subscribing to', toHex(incomingSessionId));
158
- storeUnsub = statementStore.subscribeStatements([incomingSessionId], statements => {
159
- console.info('[session] subscribeStatements callback fired — statements:', statements.length);
160
- for (const statement of statements) {
155
+ storeUnsub = statementStore.subscribeStatements({ matchAll: [incomingSessionId] }, page => {
156
+ for (const statement of page.statements) {
161
157
  processIncomingStatement(statement);
162
158
  }
163
159
  });
164
160
  // Subscribe to outgoing topic to receive peer ACK responses.
165
161
  // Only process response-type statements — request-type statements on this topic
166
162
  // are our own submissions echoed back and must be ignored.
167
- responseStoreUnsub = statementStore.subscribeStatements([outgoingSessionId], statements => {
163
+ responseStoreUnsub = statementStore.subscribeStatements({ matchAll: [outgoingSessionId] }, ({ statements }) => {
168
164
  for (const statement of statements) {
169
165
  processIncomingStatement(statement, true);
170
166
  }
@@ -172,8 +168,8 @@ export function createSession({ localAccount, remoteAccount, statementStore, enc
172
168
  }
173
169
  async function init() {
174
170
  const [ownResult, peerResult] = await Promise.all([
175
- statementStore.queryStatements([outgoingSessionId]),
176
- statementStore.queryStatements([incomingSessionId]),
171
+ statementStore.queryStatements({ matchAll: [outgoingSessionId] }),
172
+ statementStore.queryStatements({ matchAll: [incomingSessionId] }),
177
173
  ]);
178
174
  if (ownResult.isErr() || peerResult.isErr())
179
175
  return;
@@ -291,7 +287,6 @@ export function createSession({ localAccount, remoteAccount, statementStore, enc
291
287
  callback: callback,
292
288
  };
293
289
  subscribers.push(sub);
294
- console.info('[session] subscribe: subscriber count now', subscribers.length);
295
290
  ensureStoreSubscription();
296
291
  // Deliver buffered init messages to this subscriber
297
292
  if (bufferedMessages.length > 0) {
@@ -301,10 +296,8 @@ export function createSession({ localAccount, remoteAccount, statementStore, enc
301
296
  }
302
297
  return () => {
303
298
  subscribers = subscribers.filter(s => s !== sub);
304
- console.info('[session] unsubscribe: subscriber count now', subscribers.length);
305
299
  if (subscribers.length === 0) {
306
300
  if (storeUnsub) {
307
- console.warn('[session] ALL subscribers removed — killing store subscription!');
308
301
  storeUnsub();
309
302
  storeUnsub = null;
310
303
  }
@@ -5,33 +5,18 @@ import { describe, expect, it, vi } from 'vitest';
5
5
  import { createAccountId, createLocalSessionAccount, createRemoteSessionAccount } from '../model/sessionAccount.js';
6
6
  import { StatementData } from './scale/statementData.js';
7
7
  import { createSession, nextExpiry } from './session.js';
8
- // ── test helpers ──────────────────────────────────────────────────────────────
8
+ import { createSr25519Prover } from './statementProver.js';
9
9
  function makeAccounts() {
10
10
  const localAccount = createLocalSessionAccount(createAccountId(new Uint8Array(32).fill(1)));
11
11
  const remoteAccount = createRemoteSessionAccount(createAccountId(new Uint8Array(32).fill(2)), new Uint8Array(32).fill(3));
12
12
  return { localAccount, remoteAccount };
13
13
  }
14
- function makeEncryption() {
14
+ function mockEncryption() {
15
15
  return {
16
16
  encrypt: (data) => ok(data),
17
17
  decrypt: (data) => ok(data),
18
18
  };
19
19
  }
20
- function makeProver() {
21
- return {
22
- generateMessageProof: s => okAsync({
23
- ...s,
24
- proof: {
25
- type: 'sr25519',
26
- value: {
27
- signature: `0x${'00'.repeat(64)}`,
28
- signer: `0x${'00'.repeat(32)}`,
29
- },
30
- },
31
- }),
32
- verifyMessageProof: () => okAsync(true),
33
- };
34
- }
35
20
  function makeAdapter() {
36
21
  const unsub = vi.fn();
37
22
  return {
@@ -56,14 +41,14 @@ function makeSession(overrides) {
56
41
  localAccount,
57
42
  remoteAccount,
58
43
  statementStore: adapter,
59
- encryption: makeEncryption(),
60
- prover: makeProver(),
44
+ encryption: mockEncryption(),
45
+ prover: createSr25519Prover(new Uint8Array(64).fill(1)),
61
46
  maxRequestSize,
62
47
  });
63
48
  return { session, adapter };
64
49
  }
65
- async function flushPromises() {
66
- await new Promise(resolve => setTimeout(resolve, 0));
50
+ async function delay(ttl = 0) {
51
+ await new Promise(resolve => setTimeout(resolve, ttl));
67
52
  }
68
53
  describe('session', () => {
69
54
  describe('nextExpiry', () => {
@@ -94,7 +79,7 @@ describe('session', () => {
94
79
  describe('createSession initialization', () => {
95
80
  it('queries own and peer statements on creation', async () => {
96
81
  const { adapter } = makeSession();
97
- await flushPromises();
82
+ await delay();
98
83
  expect(adapter.queryStatements).toHaveBeenCalledTimes(2);
99
84
  });
100
85
  it('expiry is initialized from max own statement expiry', async () => {
@@ -110,11 +95,11 @@ describe('session', () => {
110
95
  return okAsync([]);
111
96
  });
112
97
  const { session } = makeSession(adapter);
113
- await flushPromises();
98
+ await delay();
114
99
  // Submit a message to trigger a statement — its expiry must be greater than highExpiry
115
100
  const rawCodec = Bytes();
116
101
  void session.submitRequestMessage(rawCodec, new Uint8Array([1]));
117
- await flushPromises();
102
+ await delay();
118
103
  const submittedStatement = adapter.submitStatement.mock.calls[0]?.[0];
119
104
  if (submittedStatement) {
120
105
  expect(submittedStatement.expiry).toBeGreaterThan(highExpiry);
@@ -131,7 +116,7 @@ describe('session', () => {
131
116
  return okAsync([]);
132
117
  });
133
118
  const { session } = makeSession(adapter);
134
- await flushPromises();
119
+ await delay();
135
120
  // Register subscriber AFTER init — buffered incoming request should be delivered
136
121
  const callback = vi.fn();
137
122
  session.subscribe(Bytes(), callback);
@@ -140,11 +125,10 @@ describe('session', () => {
140
125
  it('transitions to active phase after queries complete', async () => {
141
126
  const { session, adapter } = makeSession();
142
127
  // Before init completes, submitRequestMessage queues the message
143
- const rawCodec = str;
144
- void session.submitRequestMessage(rawCodec, 'hello');
128
+ void session.submitRequestMessage(str, 'hello');
145
129
  // Statement should NOT be submitted yet (still initializing)
146
130
  expect(adapter.submitStatement).not.toHaveBeenCalled();
147
- await flushPromises();
131
+ await delay();
148
132
  // After init, queued messages are processed → submitStatement called
149
133
  expect(adapter.submitStatement).toHaveBeenCalled();
150
134
  });
@@ -162,11 +146,11 @@ describe('session', () => {
162
146
  return okAsync([]); // no peer response
163
147
  });
164
148
  const { session } = makeSession(adapter);
165
- await flushPromises();
149
+ await delay();
166
150
  // If outgoingRequest was restored, a new message appends to it
167
151
  const codec = str;
168
152
  void session.submitRequestMessage(codec, 'hello');
169
- await flushPromises();
153
+ await delay();
170
154
  expect(adapter.submitStatement).toHaveBeenCalled();
171
155
  });
172
156
  it('clears outgoingRequest when peer has a matching response', async () => {
@@ -183,11 +167,11 @@ describe('session', () => {
183
167
  return okAsync([]);
184
168
  });
185
169
  const { session } = makeSession(adapter);
186
- await flushPromises();
170
+ await delay();
187
171
  // No pending outgoing request — new message creates a brand new request
188
172
  const codec = str;
189
173
  void session.submitRequestMessage(codec, 'hi');
190
- await flushPromises();
174
+ await delay();
191
175
  // submitStatement called exactly once (for the new message only)
192
176
  expect(adapter.submitStatement).toHaveBeenCalledTimes(1);
193
177
  });
@@ -203,7 +187,7 @@ describe('session', () => {
203
187
  return okAsync([]);
204
188
  });
205
189
  const { session } = makeSession(adapter);
206
- await flushPromises();
190
+ await delay();
207
191
  // Calling submitResponseMessage with restored requestId should succeed
208
192
  const result = await session.submitResponseMessage(requestId, 'success');
209
193
  expect(result.isOk()).toBe(true);
@@ -222,7 +206,7 @@ describe('session', () => {
222
206
  return okAsync([peerRequest, ownResponse]);
223
207
  });
224
208
  const { session } = makeSession(adapter);
225
- await flushPromises();
209
+ await delay();
226
210
  // Already responded — submitResponseMessage should return ok without submitting again
227
211
  const submitsBefore = adapter.submitStatement.mock.calls.length;
228
212
  const result = await session.submitResponseMessage(requestId, 'success');
@@ -244,7 +228,7 @@ describe('session', () => {
244
228
  return okAsync([]);
245
229
  });
246
230
  const { session } = makeSession(adapter);
247
- await flushPromises(); // init completes
231
+ await delay(); // init completes
248
232
  const callback = vi.fn();
249
233
  session.subscribe(rawCodec, callback);
250
234
  expect(callback).toHaveBeenCalledTimes(1);
@@ -266,7 +250,7 @@ describe('session', () => {
266
250
  const { session } = makeSession(adapter);
267
251
  const callback = vi.fn();
268
252
  session.subscribe(rawCodec, callback); // before init completes
269
- await flushPromises();
253
+ await delay();
270
254
  expect(callback).toHaveBeenCalledTimes(1);
271
255
  const messages2 = callback.mock.calls[0][0];
272
256
  expect(messages2[0]?.requestId).toBe(requestId);
@@ -283,38 +267,38 @@ describe('session', () => {
283
267
  return okAsync([]);
284
268
  });
285
269
  let subscribeCallback;
286
- adapter.subscribeStatements.mockImplementation((_topics, cb) => {
270
+ adapter.subscribeStatements.mockImplementation((_filter, cb) => {
287
271
  subscribeCallback = cb;
288
272
  return vi.fn();
289
273
  });
290
274
  const { session } = makeSession(adapter);
291
- await flushPromises(); // init sees peerRequest, adds to seenStatements
275
+ await delay(); // init sees peerRequest, adds to seenStatements
292
276
  const appCallback = vi.fn();
293
277
  session.subscribe(rawCodec, appCallback);
294
278
  // Simulate subscription delivering the same statement again
295
- subscribeCallback([peerRequest]);
296
- await flushPromises();
279
+ subscribeCallback({ statements: [peerRequest], isComplete: true });
280
+ await delay();
297
281
  // Should only be called once (from buffered init message), not again from subscription
298
282
  expect(appCallback).toHaveBeenCalledTimes(1);
299
283
  });
300
284
  it('does NOT auto-send a response when an incoming request arrives', async () => {
301
285
  const requestId = 'no-auto-resp';
302
286
  const peerRequest = makeStatement({ tag: 'request', value: { requestId, data: [new Uint8Array([1])] } });
303
- const subscribeCallbacks = [];
304
287
  const adapter = makeAdapter();
305
- adapter.subscribeStatements.mockImplementation((_topics, cb) => {
288
+ const subscribeCallbacks = [];
289
+ adapter.subscribeStatements.mockImplementation((_filter, cb) => {
306
290
  subscribeCallbacks.push(cb);
307
291
  return vi.fn();
308
292
  });
309
293
  adapter.queryStatements.mockReturnValue(okAsync([]));
310
294
  const { session } = makeSession(adapter);
311
- await flushPromises();
295
+ await delay();
312
296
  const callback = vi.fn();
313
297
  session.subscribe(rawCodec, callback);
314
298
  adapter.submitStatement.mockClear();
315
299
  // Fire on the incoming topic callback (first subscription)
316
- subscribeCallbacks[0]([peerRequest]);
317
- await flushPromises();
300
+ subscribeCallbacks[0]({ statements: [peerRequest], isComplete: true });
301
+ await delay();
318
302
  // Message delivered to app callback but no automatic response submitted
319
303
  expect(callback).toHaveBeenCalled();
320
304
  expect(adapter.submitStatement).not.toHaveBeenCalled();
@@ -326,14 +310,12 @@ describe('session', () => {
326
310
  // buffered so late subscribers (simulating waitForRequestMessage called in .andThen()
327
311
  // after waitForResponseMessage resolves) still receive them.
328
312
  const subscribeCallbacks = [];
329
- const subscribeStatements = vi
330
- .fn()
331
- .mockImplementation((_topics, cb) => {
313
+ const subscribeStatements = vi.fn().mockImplementation((_filter, cb) => {
332
314
  subscribeCallbacks.push(cb);
333
315
  return vi.fn();
334
316
  });
335
317
  const { session } = makeSession({ subscribeStatements });
336
- await flushPromises();
318
+ await delay();
337
319
  // Register a dummy subscriber to activate the store subscription (simulates
338
320
  // any pre-existing subscriber in the session, e.g. the app listening for messages).
339
321
  const dummyUnsub = session.subscribe(rawCodec, vi.fn());
@@ -345,8 +327,8 @@ describe('session', () => {
345
327
  // Peer request arrives on the incoming topic (first subscription) while the
346
328
  // dummy subscriber is active but waitForRequestMessage hasn't registered its
347
329
  // subscriber yet (the race condition scenario).
348
- subscribeCallbacks[0]([peerRequest]);
349
- await flushPromises();
330
+ subscribeCallbacks[0]({ statements: [peerRequest], isComplete: true });
331
+ await delay();
350
332
  // Now the late subscriber registers (simulates waitForRequestMessage being called
351
333
  // in the .andThen() chain after waitForResponseMessage resolves).
352
334
  const lateCallback = vi.fn();
@@ -387,17 +369,15 @@ describe('session', () => {
387
369
  });
388
370
  it('delivers peer response from outgoing topic subscription to subscribers', async () => {
389
371
  const subscribeCallbacks = [];
390
- const subscribeStatements = vi
391
- .fn()
392
- .mockImplementation((_topics, cb) => {
372
+ const subscribeStatements = vi.fn().mockImplementation((_topics, cb) => {
393
373
  subscribeCallbacks.push(cb);
394
374
  return vi.fn();
395
375
  });
396
376
  const { session, adapter } = makeSession({ subscribeStatements });
397
- await flushPromises();
377
+ await delay();
398
378
  // Submit a request so the session has an outgoingRequest
399
379
  void session.submitRequestMessage(rawCodec, new Uint8Array([1]));
400
- await flushPromises();
380
+ await delay();
401
381
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
402
382
  const submitted = adapter.submitStatement.mock.calls[0][0];
403
383
  const decoded = StatementData.dec(submitted.data);
@@ -410,8 +390,8 @@ describe('session', () => {
410
390
  tag: 'response',
411
391
  value: { requestId, responseCode: 'success' },
412
392
  });
413
- subscribeCallbacks[1]([responseStatement]);
414
- await flushPromises();
393
+ subscribeCallbacks[1]({ statements: [responseStatement], isComplete: true });
394
+ await delay();
415
395
  // Subscriber should receive the response
416
396
  const allCalls = callback.mock.calls.flat();
417
397
  const responseMessages = allCalls.flat().filter((m) => m.type === 'response');
@@ -419,14 +399,12 @@ describe('session', () => {
419
399
  });
420
400
  it('ignores request-type statements from outgoing topic subscription', async () => {
421
401
  const subscribeCallbacks = [];
422
- const subscribeStatements = vi
423
- .fn()
424
- .mockImplementation((_topics, cb) => {
402
+ const subscribeStatements = vi.fn().mockImplementation((_topics, cb) => {
425
403
  subscribeCallbacks.push(cb);
426
404
  return vi.fn();
427
405
  });
428
406
  const { session } = makeSession({ subscribeStatements });
429
- await flushPromises();
407
+ await delay();
430
408
  const callback = vi.fn();
431
409
  session.subscribe(rawCodec, callback);
432
410
  callback.mockClear(); // clear any buffered init messages
@@ -435,8 +413,8 @@ describe('session', () => {
435
413
  tag: 'request',
436
414
  value: { requestId: 'own-req', data: [new Uint8Array([1])] },
437
415
  });
438
- subscribeCallbacks[1]([ownRequest]);
439
- await flushPromises();
416
+ subscribeCallbacks[1]({ statements: [ownRequest], isComplete: true });
417
+ await delay();
440
418
  // Should NOT be delivered to subscriber (filtered by responsesOnly flag)
441
419
  expect(callback).not.toHaveBeenCalled();
442
420
  });
@@ -454,7 +432,7 @@ describe('session', () => {
454
432
  return okAsync([]);
455
433
  });
456
434
  const { session } = makeSession(adapter);
457
- await flushPromises();
435
+ await delay();
458
436
  await session.submitResponseMessage(requestId, 'success');
459
437
  const submitsAfterFirst = adapter.submitStatement.mock.calls.length;
460
438
  await session.submitResponseMessage(requestId, 'success'); // second call
@@ -462,7 +440,7 @@ describe('session', () => {
462
440
  });
463
441
  it('returns error when requestId does not match incomingRequest', async () => {
464
442
  const { session } = makeSession();
465
- await flushPromises();
443
+ await delay();
466
444
  const result = await session.submitResponseMessage('wrong-id', 'success');
467
445
  expect(result.isErr()).toBe(true);
468
446
  });
@@ -471,43 +449,41 @@ describe('session', () => {
471
449
  const rawCodec = Bytes();
472
450
  it('sends a single statement for the first message', async () => {
473
451
  const { session, adapter } = makeSession();
474
- await flushPromises();
452
+ await delay();
475
453
  void session.submitRequestMessage(rawCodec, new Uint8Array([1, 2, 3]));
476
- await flushPromises();
454
+ await delay();
477
455
  expect(adapter.submitStatement).toHaveBeenCalledTimes(1);
478
456
  });
479
457
  it('appends second message to existing request (resubmits with new requestId)', async () => {
480
458
  const { session, adapter } = makeSession();
481
- await flushPromises();
459
+ await delay();
482
460
  void session.submitRequestMessage(rawCodec, new Uint8Array([1]));
483
461
  void session.submitRequestMessage(rawCodec, new Uint8Array([2]));
484
- await flushPromises();
462
+ await delay();
485
463
  // Two submits: first for msg1, second for msg1+msg2 batched
486
464
  expect(adapter.submitStatement).toHaveBeenCalledTimes(2);
487
465
  });
488
466
  it('queues message that exceeds maxRequestSize', async () => {
489
467
  const { session, adapter } = makeSession({ maxRequestSize: 5 });
490
- await flushPromises();
468
+ await delay();
491
469
  void session.submitRequestMessage(rawCodec, new Uint8Array([1, 2, 3])); // 3 bytes — fits
492
470
  void session.submitRequestMessage(rawCodec, new Uint8Array([4, 5, 6, 7])); // 4 bytes — doesn't fit with existing
493
- await flushPromises();
471
+ await delay();
494
472
  // Only first message sent; second is queued
495
473
  expect(adapter.submitStatement).toHaveBeenCalledTimes(1);
496
474
  });
497
475
  it('drains message queue after response received', async () => {
498
476
  let subscribeCallback;
499
- const subscribeStatements = vi
500
- .fn()
501
- .mockImplementation((_topics, cb) => {
477
+ const subscribeStatements = vi.fn().mockImplementation((_filter, cb) => {
502
478
  subscribeCallback = cb;
503
479
  return vi.fn();
504
480
  });
505
481
  const { session, adapter } = makeSession({ maxRequestSize: 5, subscribeStatements });
506
- await flushPromises();
482
+ await delay();
507
483
  session.subscribe(Bytes(), vi.fn()); // ensure store subscription is active
508
484
  void session.submitRequestMessage(rawCodec, new Uint8Array([1, 2, 3])); // sent
509
485
  void session.submitRequestMessage(rawCodec, new Uint8Array([4, 5, 6])); // queued (doesn't fit)
510
- await flushPromises();
486
+ await delay();
511
487
  const submitCountBefore = adapter.submitStatement.mock.calls.length;
512
488
  // Simulate peer responding to the first request
513
489
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
@@ -518,34 +494,33 @@ describe('session', () => {
518
494
  tag: 'response',
519
495
  value: { requestId: respondingRequestId, responseCode: 'success' },
520
496
  });
521
- subscribeCallback([responseStatement]);
522
- await flushPromises();
497
+ subscribeCallback({ statements: [responseStatement], isComplete: true });
498
+ await delay();
523
499
  // Queued message should now be submitted
524
500
  expect(adapter.submitStatement.mock.calls.length).toBeGreaterThan(submitCountBefore);
525
501
  });
526
502
  it('waitForResponseMessage resolves when response arrives for batch', async () => {
527
503
  let subscribeCallback;
528
- const subscribeStatements = vi
529
- .fn()
530
- .mockImplementation((_topics, cb) => {
504
+ const subscribeStatements = vi.fn().mockImplementation((_filter, cb) => {
531
505
  subscribeCallback = cb;
532
506
  return vi.fn();
533
507
  });
534
508
  const { session, adapter } = makeSession({ subscribeStatements });
535
- await flushPromises();
509
+ await delay();
536
510
  session.subscribe(Bytes(), vi.fn()); // ensure store subscription is active
537
511
  const submitResult = await session.submitRequestMessage(rawCodec, new Uint8Array([1]));
538
512
  const token = submitResult.unwrapOr({ requestId: '' }).requestId;
539
- await flushPromises();
513
+ await delay();
540
514
  const responsePromise = session.waitForResponseMessage(token);
541
515
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
542
516
  const lastStatement = adapter.submitStatement.mock.calls[adapter.submitStatement.mock.calls.length - 1][0];
543
517
  const decoded = StatementData.dec(lastStatement.data);
544
518
  const respondingId = decoded.tag === 'request' ? decoded.value.requestId : '';
545
- subscribeCallback([
546
- makeStatement({ tag: 'response', value: { requestId: respondingId, responseCode: 'success' } }),
547
- ]);
548
- await flushPromises();
519
+ subscribeCallback({
520
+ statements: [makeStatement({ tag: 'response', value: { requestId: respondingId, responseCode: 'success' } })],
521
+ isComplete: true,
522
+ });
523
+ await delay();
549
524
  const result = await responsePromise;
550
525
  expect(result.isOk()).toBe(true);
551
526
  expect(result.unwrapOr({ responseCode: 'unknown' }).responseCode).toBe('success');
@@ -1,7 +1,7 @@
1
1
  import { getStatementSigner, statementCodec } from '@novasamatech/sdk-statement';
2
2
  import { compact } from '@polkadot-api/substrate-bindings';
3
- import { fromHex } from '@polkadot-api/utils';
4
3
  import { errAsync, fromPromise, fromThrowable, okAsync } from 'neverthrow';
4
+ import { fromHex } from 'polkadot-api/utils';
5
5
  import { deriveSr25519PublicKey, signWithSr25519Secret, verifySr25519Signature } from '../crypto.js';
6
6
  import { toError } from '../helpers.js';
7
7
  export function createSr25519Prover(secret) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@novasamatech/statement-store",
3
3
  "type": "module",
4
- "version": "0.6.18",
4
+ "version": "0.7.0-1",
5
5
  "description": "Statement store integration",
6
6
  "license": "Apache-2.0",
7
7
  "repository": {
@@ -25,15 +25,16 @@
25
25
  "README.md"
26
26
  ],
27
27
  "dependencies": {
28
+ "@novasamatech/scale": "0.7.0-1",
28
29
  "@novasamatech/sdk-statement": "^0.5.0",
29
- "@polkadot-api/substrate-bindings": "^0.17.0",
30
- "@polkadot-api/substrate-client": "^0.5.0",
31
- "@polkadot-api/utils": "0.2.0",
32
- "@polkadot-labs/hdkd-helpers": "^0.0.28",
33
- "@noble/hashes": "2.0.1",
34
- "@noble/ciphers": "2.1.1",
30
+ "@polkadot-api/substrate-bindings": "^0.20.0",
31
+ "@polkadot-api/substrate-client": "^0.7.0",
32
+ "@polkadot-labs/hdkd-helpers": "^0.0.29",
33
+ "@noble/hashes": "2.2.0",
34
+ "@noble/ciphers": "2.2.0",
35
35
  "@scure/sr25519": "1.0.0",
36
- "polkadot-api": "^1.23.3",
36
+ "polkadot-api": ">=2",
37
+ "nanoid": "5.1.9",
37
38
  "neverthrow": "^8.2.0",
38
39
  "scale-ts": "1.6.1"
39
40
  },