@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.
- package/dist/adapter/lazyClient.d.ts +1 -2
- package/dist/adapter/rpc.js +22 -22
- package/dist/adapter/types.d.ts +11 -2
- package/dist/crypto.d.ts +1 -1
- package/dist/model/session.js +1 -1
- package/dist/session/encyption.js +1 -1
- package/dist/session/session.js +7 -14
- package/dist/session/session.spec.js +63 -88
- package/dist/session/statementProver.js +1 -1
- package/package.json +9 -8
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { RequestFn, SubscribeFn } from '@novasamatech/sdk-statement';
|
|
2
|
-
import type { JsonRpcProvider } from '
|
|
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;
|
package/dist/adapter/rpc.js
CHANGED
|
@@ -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
|
|
7
|
-
|
|
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
|
|
10
|
-
if (
|
|
11
|
-
return '
|
|
12
|
-
|
|
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
|
-
|
|
32
|
+
const list = callbacks.get(key);
|
|
29
33
|
if (!list)
|
|
30
34
|
return [];
|
|
31
|
-
|
|
32
|
-
if (
|
|
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(
|
|
42
|
-
|
|
43
|
-
return fromPromise(sdk.getStatements(filter), toError);
|
|
43
|
+
queryStatements(filter) {
|
|
44
|
+
return fromPromise(sdk.getStatements(toSdkTopicFilter(filter)), toError);
|
|
44
45
|
},
|
|
45
|
-
subscribeStatements(
|
|
46
|
-
const key = createKey(
|
|
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;
|
package/dist/adapter/types.d.ts
CHANGED
|
@@ -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(
|
|
5
|
-
subscribeStatements(
|
|
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;
|
package/dist/model/session.js
CHANGED
|
@@ -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
|
package/dist/session/session.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
60
|
-
prover:
|
|
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
|
|
66
|
-
await new Promise(resolve => setTimeout(resolve,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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((
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
452
|
+
await delay();
|
|
475
453
|
void session.submitRequestMessage(rawCodec, new Uint8Array([1, 2, 3]));
|
|
476
|
-
await
|
|
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
|
|
459
|
+
await delay();
|
|
482
460
|
void session.submitRequestMessage(rawCodec, new Uint8Array([1]));
|
|
483
461
|
void session.submitRequestMessage(rawCodec, new Uint8Array([2]));
|
|
484
|
-
await
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
30
|
-
"@polkadot-api/substrate-client": "^0.
|
|
31
|
-
"@polkadot-
|
|
32
|
-
"@
|
|
33
|
-
"@noble/
|
|
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": "
|
|
36
|
+
"polkadot-api": ">=2",
|
|
37
|
+
"nanoid": "5.1.9",
|
|
37
38
|
"neverthrow": "^8.2.0",
|
|
38
39
|
"scale-ts": "1.6.1"
|
|
39
40
|
},
|