@novasamatech/statement-store 0.8.0-1 → 0.8.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/crypto.d.ts +13 -2
- package/dist/crypto.js +22 -4
- package/dist/crypto.spec.d.ts +1 -0
- package/dist/crypto.spec.js +90 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/session/session.js +34 -3
- package/dist/session/session.spec.js +91 -1
- package/dist/session/types.d.ts +9 -0
- package/dist/substrateSr25519.d.ts +5 -0
- package/dist/substrateSr25519.js +25 -0
- package/package.json +4 -3
package/dist/crypto.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { ensureSubstrateSlotSr25519Ready } from '@novasamatech/substrate-slot-sr25519-wasm';
|
|
1
2
|
import type { Codec } from 'scale-ts';
|
|
3
|
+
export { ensureSubstrateSlotSr25519Ready };
|
|
4
|
+
export { ensureSubstrateSr25519Ready } from './substrateSr25519.js';
|
|
2
5
|
export declare function BrandedBytesCodec<T extends Uint8Array>(length?: number): Codec<T>;
|
|
3
6
|
export declare function stringToBytes(str: string): Uint8Array<ArrayBuffer>;
|
|
4
7
|
/**
|
|
@@ -7,6 +10,14 @@ export declare function stringToBytes(str: string): Uint8Array<ArrayBuffer>;
|
|
|
7
10
|
export declare function khash(secret: Uint8Array, message: Uint8Array): Uint8Array<ArrayBufferLike> & Uint8Array<ArrayBuffer>;
|
|
8
11
|
export declare function createSr25519Secret(entropy: Uint8Array, derivation?: string): Uint8Array<ArrayBufferLike>;
|
|
9
12
|
export declare function createSr25519Derivation(secret: Uint8Array, derivation: string): Uint8Array<ArrayBufferLike>;
|
|
10
|
-
|
|
11
|
-
export declare function
|
|
13
|
+
/** Ed25519-expanded secret (scure HDKD / `createSr25519Secret`). */
|
|
14
|
+
export declare function deriveSr25519PublicKey(secret: Uint8Array): Uint8Array<ArrayBufferLike>;
|
|
15
|
+
export declare function signWithSr25519Secret(secret: Uint8Array, message: Uint8Array): Uint8Array<ArrayBufferLike>;
|
|
12
16
|
export declare function verifySr25519Signature(message: Uint8Array, signature: Uint8Array, publicKey: Uint8Array): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Substrate slot secret (`privateKey || nonce`, 64 bytes) from mobile `SlotAccountKey`.
|
|
19
|
+
* Matches Android `deriveAccountId()` / `Sr25519.getPublicKeyFromSecret`.
|
|
20
|
+
*/
|
|
21
|
+
export declare function deriveSlotAccountPublicKey(secret: Uint8Array): Uint8Array<ArrayBufferLike>;
|
|
22
|
+
export declare function signSlotAccountSecret(secret: Uint8Array, message: Uint8Array): Uint8Array<ArrayBufferLike>;
|
|
23
|
+
export declare function verifySlotAccountSignature(message: Uint8Array, signature: Uint8Array, publicKey: Uint8Array): boolean;
|
package/dist/crypto.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { blake2b } from '@noble/hashes/blake2.js';
|
|
2
|
+
import { deriveSlotAccountPublicKey as deriveSlotPublicKey, ensureSubstrateSlotSr25519Ready, signSlotAccountSecret as signSlotSecret, verifySlotAccountSignature as verifySlotSignature, } from '@novasamatech/substrate-slot-sr25519-wasm';
|
|
2
3
|
import { entropyToMiniSecret } from '@polkadot-labs/hdkd-helpers';
|
|
3
|
-
import { HDKD as sr25519HDKD,
|
|
4
|
+
import { HDKD as sr25519HDKD, secretFromSeed as sr25519SecretFromSeed } from '@scure/sr25519';
|
|
4
5
|
import { Bytes, str, u64 } from 'scale-ts';
|
|
6
|
+
import { substrateSr25519PublicKey, substrateSr25519Sign, substrateSr25519Verify } from './substrateSr25519.js';
|
|
7
|
+
export { ensureSubstrateSlotSr25519Ready };
|
|
8
|
+
export { ensureSubstrateSr25519Ready } from './substrateSr25519.js';
|
|
5
9
|
export function BrandedBytesCodec(length) {
|
|
6
10
|
return Bytes(length);
|
|
7
11
|
}
|
|
@@ -60,12 +64,26 @@ export function createSr25519Derivation(secret, derivation) {
|
|
|
60
64
|
}
|
|
61
65
|
}, secret);
|
|
62
66
|
}
|
|
67
|
+
/** Ed25519-expanded secret (scure HDKD / `createSr25519Secret`). */
|
|
63
68
|
export function deriveSr25519PublicKey(secret) {
|
|
64
|
-
return
|
|
69
|
+
return substrateSr25519PublicKey(secret);
|
|
65
70
|
}
|
|
66
71
|
export function signWithSr25519Secret(secret, message) {
|
|
67
|
-
return
|
|
72
|
+
return substrateSr25519Sign(secret, message);
|
|
68
73
|
}
|
|
69
74
|
export function verifySr25519Signature(message, signature, publicKey) {
|
|
70
|
-
return
|
|
75
|
+
return substrateSr25519Verify(message, signature, publicKey);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Substrate slot secret (`privateKey || nonce`, 64 bytes) from mobile `SlotAccountKey`.
|
|
79
|
+
* Matches Android `deriveAccountId()` / `Sr25519.getPublicKeyFromSecret`.
|
|
80
|
+
*/
|
|
81
|
+
export function deriveSlotAccountPublicKey(secret) {
|
|
82
|
+
return deriveSlotPublicKey(secret);
|
|
83
|
+
}
|
|
84
|
+
export function signSlotAccountSecret(secret, message) {
|
|
85
|
+
return signSlotSecret(secret, message);
|
|
86
|
+
}
|
|
87
|
+
export function verifySlotAccountSignature(message, signature, publicKey) {
|
|
88
|
+
return verifySlotSignature(message, signature, publicKey);
|
|
71
89
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { blake2b } from '@noble/hashes/blake2.js';
|
|
2
|
+
import { ensureSubstrateSlotSr25519Ready, substrateSlotSecretFromSeedBytes, } from '@novasamatech/substrate-slot-sr25519-wasm';
|
|
3
|
+
import { mnemonicToEntropy, mnemonicToMiniSecret } from '@polkadot-labs/hdkd-helpers';
|
|
4
|
+
import * as schnorrkelWasm from '@polkadot-labs/schnorrkel-wasm';
|
|
5
|
+
import { HDKD, secretFromSeed } from '@scure/sr25519';
|
|
6
|
+
import { str, u64 } from 'scale-ts';
|
|
7
|
+
import { beforeAll, describe, expect, it } from 'vitest';
|
|
8
|
+
import { createSr25519Secret, deriveSlotAccountPublicKey, deriveSr25519PublicKey, signSlotAccountSecret, signWithSr25519Secret, verifySlotAccountSignature, verifySr25519Signature, } from './crypto.js';
|
|
9
|
+
const { sr25519_derive_keypair_hard, sr25519_keypair_from_seed, sr25519_pubkey } = schnorrkelWasm;
|
|
10
|
+
const initSchnorrkelWasm = schnorrkelWasm.init;
|
|
11
|
+
const DEV_MNEMONIC = 'bottom drive obey lake curtain smoke basket hold race lonely fit walk';
|
|
12
|
+
const ALLOWANCE_PATH = '//allowance//bulletin//localhost:5173';
|
|
13
|
+
const toHex = (bytes) => `0x${[...bytes].map(b => b.toString(16).padStart(2, '0')).join('')}`;
|
|
14
|
+
function createChainCode(derivation) {
|
|
15
|
+
const encoded = /^\d+$/.test(derivation) ? u64.enc(BigInt(derivation)) : str.enc(derivation);
|
|
16
|
+
if (encoded.length > 32) {
|
|
17
|
+
return blake2b(encoded, { dkLen: 32 });
|
|
18
|
+
}
|
|
19
|
+
const chainCode = new Uint8Array(32);
|
|
20
|
+
chainCode.set(encoded);
|
|
21
|
+
return chainCode;
|
|
22
|
+
}
|
|
23
|
+
function wasmDeriveAllowanceKeypair(miniSecret) {
|
|
24
|
+
initSchnorrkelWasm();
|
|
25
|
+
let pair = sr25519_keypair_from_seed(miniSecret);
|
|
26
|
+
for (const match of ALLOWANCE_PATH.matchAll(/(\/{1,2})([^/]+)/g)) {
|
|
27
|
+
const type = match[1];
|
|
28
|
+
const code = match[2];
|
|
29
|
+
if (!type || !code) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (type !== '//') {
|
|
33
|
+
throw new Error('soft junction not expected in test path');
|
|
34
|
+
}
|
|
35
|
+
pair = sr25519_derive_keypair_hard(pair, createChainCode(code));
|
|
36
|
+
}
|
|
37
|
+
return pair;
|
|
38
|
+
}
|
|
39
|
+
describe('sr25519 crypto (Substrate-compatible)', () => {
|
|
40
|
+
beforeAll(async () => {
|
|
41
|
+
initSchnorrkelWasm();
|
|
42
|
+
await ensureSubstrateSlotSr25519Ready();
|
|
43
|
+
});
|
|
44
|
+
it('derives the same public key as schnorrkel-wasm for an allowance path secret', () => {
|
|
45
|
+
const entropy = mnemonicToEntropy(DEV_MNEMONIC);
|
|
46
|
+
const miniSecret = mnemonicToMiniSecret(DEV_MNEMONIC);
|
|
47
|
+
const secret = createSr25519Secret(entropy, ALLOWANCE_PATH);
|
|
48
|
+
const wasmPair = wasmDeriveAllowanceKeypair(miniSecret);
|
|
49
|
+
const wasmPublicKey = wasmPair.slice(64, 96);
|
|
50
|
+
expect(deriveSr25519PublicKey(secret)).toEqual(wasmPublicKey);
|
|
51
|
+
expect(toHex(deriveSr25519PublicKey(secret))).toBe(toHex(wasmPublicKey));
|
|
52
|
+
});
|
|
53
|
+
it('derives slot account pubkey via SecretKey::from_bytes (mobile SlotAccountKey shape)', () => {
|
|
54
|
+
const miniSecret = mnemonicToMiniSecret(DEV_MNEMONIC);
|
|
55
|
+
const slotSecret = substrateSlotSecretFromSeedBytes(miniSecret);
|
|
56
|
+
const wasmPublicKey = deriveSlotAccountPublicKey(slotSecret);
|
|
57
|
+
expect(wasmPublicKey).not.toEqual(sr25519_pubkey(slotSecret));
|
|
58
|
+
expect(deriveSlotAccountPublicKey(slotSecret)).toEqual(wasmPublicKey);
|
|
59
|
+
});
|
|
60
|
+
it('signs and verifies slot-account secrets with the substrate context', () => {
|
|
61
|
+
const miniSecret = mnemonicToMiniSecret(DEV_MNEMONIC);
|
|
62
|
+
const slotSecret = substrateSlotSecretFromSeedBytes(miniSecret);
|
|
63
|
+
const publicKey = deriveSlotAccountPublicKey(slotSecret);
|
|
64
|
+
const message = new TextEncoder().encode('substrate-context-test');
|
|
65
|
+
const signature = signSlotAccountSecret(slotSecret, message);
|
|
66
|
+
expect(verifySlotAccountSignature(message, signature, publicKey)).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
it('signs and verifies ed25519-expanded secrets with the substrate context', () => {
|
|
69
|
+
const secret = createSr25519Secret(mnemonicToEntropy(DEV_MNEMONIC), ALLOWANCE_PATH);
|
|
70
|
+
const publicKey = deriveSr25519PublicKey(secret);
|
|
71
|
+
const message = new TextEncoder().encode('substrate-context-test');
|
|
72
|
+
const signature = signWithSr25519Secret(secret, message);
|
|
73
|
+
expect(verifySr25519Signature(message, signature, publicKey)).toBe(true);
|
|
74
|
+
});
|
|
75
|
+
it('matches scure HDKD secret bytes for wasm-derived allowance keys', () => {
|
|
76
|
+
const miniSecret = mnemonicToMiniSecret(DEV_MNEMONIC);
|
|
77
|
+
let scureSecret = secretFromSeed(miniSecret);
|
|
78
|
+
for (const match of ALLOWANCE_PATH.matchAll(/(\/{1,2})([^/]+)/g)) {
|
|
79
|
+
const type = match[1];
|
|
80
|
+
const code = match[2];
|
|
81
|
+
if (!type || !code) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const cc = createChainCode(code);
|
|
85
|
+
scureSecret = Uint8Array.from(type === '//' ? HDKD.secretHard(scureSecret, cc) : HDKD.secretSoft(scureSecret, cc));
|
|
86
|
+
}
|
|
87
|
+
const wasmSecret = wasmDeriveAllowanceKeypair(miniSecret).slice(0, 64);
|
|
88
|
+
expect(toHex(scureSecret)).toBe(toHex(wasmSecret));
|
|
89
|
+
});
|
|
90
|
+
});
|
package/dist/index.d.ts
CHANGED
|
@@ -16,4 +16,5 @@ export { createLazyClient } from './adapter/lazyClient.js';
|
|
|
16
16
|
export type { StatementStoreAdapter } from './adapter/types.js';
|
|
17
17
|
export { AccountFullError, AlreadyExpiredError, BadProofError, DataTooLargeError, EncodingTooLargeError, ExpiryTooLowError, InternalStoreError, KnownExpiredError, NoAllowanceError, NoProofError, StorageFullError, } from './adapter/types.js';
|
|
18
18
|
export { createPapiStatementStoreAdapter } from './adapter/rpc.js';
|
|
19
|
-
export { createSr25519Derivation, createSr25519Secret, deriveSr25519PublicKey, khash, signWithSr25519Secret, verifySr25519Signature, } from './crypto.js';
|
|
19
|
+
export { createSr25519Derivation, createSr25519Secret, deriveSlotAccountPublicKey, deriveSr25519PublicKey, ensureSubstrateSlotSr25519Ready, ensureSubstrateSr25519Ready, khash, signSlotAccountSecret, signWithSr25519Secret, verifySlotAccountSignature, verifySr25519Signature, } from './crypto.js';
|
|
20
|
+
export { substrateSr25519PublicKey } from './substrateSr25519.js';
|
package/dist/index.js
CHANGED
|
@@ -8,4 +8,5 @@ export { DecodingError, DecryptionError, UnknownError } from './session/error.js
|
|
|
8
8
|
export { createLazyClient } from './adapter/lazyClient.js';
|
|
9
9
|
export { AccountFullError, AlreadyExpiredError, BadProofError, DataTooLargeError, EncodingTooLargeError, ExpiryTooLowError, InternalStoreError, KnownExpiredError, NoAllowanceError, NoProofError, StorageFullError, } from './adapter/types.js';
|
|
10
10
|
export { createPapiStatementStoreAdapter } from './adapter/rpc.js';
|
|
11
|
-
export { createSr25519Derivation, createSr25519Secret, deriveSr25519PublicKey, khash, signWithSr25519Secret, verifySr25519Signature, } from './crypto.js';
|
|
11
|
+
export { createSr25519Derivation, createSr25519Secret, deriveSlotAccountPublicKey, deriveSr25519PublicKey, ensureSubstrateSlotSr25519Ready, ensureSubstrateSr25519Ready, khash, signSlotAccountSecret, signWithSr25519Secret, verifySlotAccountSignature, verifySr25519Signature, } from './crypto.js';
|
|
12
|
+
export { substrateSr25519PublicKey } from './substrateSr25519.js';
|
package/dist/session/session.js
CHANGED
|
@@ -32,9 +32,7 @@ export function createSession({ localAccount, remoteAccount, statementStore, enc
|
|
|
32
32
|
const bufferedMessages = [];
|
|
33
33
|
let storeUnsub = null;
|
|
34
34
|
let responseStoreUnsub = null;
|
|
35
|
-
function
|
|
36
|
-
state.expiry = nextExpiry(state.expiry);
|
|
37
|
-
const expiry = state.expiry;
|
|
35
|
+
function submitStatementAt(expiry, channel, topicSessionId, data) {
|
|
38
36
|
return encryption
|
|
39
37
|
.encrypt(data)
|
|
40
38
|
.map(encrypted => ({
|
|
@@ -46,6 +44,10 @@ export function createSession({ localAccount, remoteAccount, statementStore, enc
|
|
|
46
44
|
.asyncAndThen(prover.generateMessageProof)
|
|
47
45
|
.andThen(statementStore.submitStatement);
|
|
48
46
|
}
|
|
47
|
+
function submitStatementData(channel, topicSessionId, data) {
|
|
48
|
+
state.expiry = nextExpiry(state.expiry);
|
|
49
|
+
return submitStatementAt(state.expiry, channel, topicSessionId, data);
|
|
50
|
+
}
|
|
49
51
|
function encodeAndSubmitRequest(requestId, messages) {
|
|
50
52
|
const encode = fromThrowable(StatementData.enc, toError);
|
|
51
53
|
encode({ tag: 'request', value: { requestId, data: messages } })
|
|
@@ -261,6 +263,10 @@ export function createSession({ localAccount, remoteAccount, statementStore, enc
|
|
|
261
263
|
rejectFn = rej;
|
|
262
264
|
});
|
|
263
265
|
state.pendingDelivery.set(token, { resolve: resolveFn, reject: rejectFn, promise });
|
|
266
|
+
// Ensure a rejection from clearOutgoingStatement()/dispose() is always handled,
|
|
267
|
+
// even when no caller attached via waitForResponseMessage(); the real waiter
|
|
268
|
+
// still receives the rejection through its own handler.
|
|
269
|
+
promise.catch(() => undefined);
|
|
264
270
|
if (state.phase === 'initialization') {
|
|
265
271
|
state.messageQueue.push({ encoded, token });
|
|
266
272
|
}
|
|
@@ -332,6 +338,31 @@ export function createSession({ localAccount, remoteAccount, statementStore, enc
|
|
|
332
338
|
}
|
|
333
339
|
};
|
|
334
340
|
},
|
|
341
|
+
clearOutgoingStatement() {
|
|
342
|
+
const outgoing = state.outgoingRequest;
|
|
343
|
+
// Reuse the current expiry (do NOT call nextExpiry): the live batch was last
|
|
344
|
+
// submitted at state.expiry, so an empty statement at the same expiry on the
|
|
345
|
+
// same channel supersedes it. The store rejects only a strictly lower expiry.
|
|
346
|
+
const expiry = state.expiry;
|
|
347
|
+
// Always drop local outgoing state and reject pending waiters up-front,
|
|
348
|
+
// regardless of which path follows. This covers messages queued before the
|
|
349
|
+
// batch went out (e.g. during init, while outgoingRequest is still null) and
|
|
350
|
+
// guarantees cleanup even if the superseding submission below fails — the
|
|
351
|
+
// caller still receives any submission error.
|
|
352
|
+
state.outgoingRequest = null;
|
|
353
|
+
state.messageQueue = [];
|
|
354
|
+
rejectAllPending(new Error('Outgoing batch aborted'));
|
|
355
|
+
if (outgoing === null)
|
|
356
|
+
return okAsync(undefined);
|
|
357
|
+
const requestId = outgoing.requestIds[outgoing.requestIds.length - 1];
|
|
358
|
+
const encoded = fromThrowable(StatementData.enc, toError)({
|
|
359
|
+
tag: 'request',
|
|
360
|
+
value: { requestId, data: [] },
|
|
361
|
+
});
|
|
362
|
+
if (encoded.isErr())
|
|
363
|
+
return errAsync(encoded.error);
|
|
364
|
+
return submitStatementAt(expiry, createRequestChannel(outgoingSessionId), outgoingSessionId, encoded.value);
|
|
365
|
+
},
|
|
335
366
|
dispose() {
|
|
336
367
|
storeUnsub?.();
|
|
337
368
|
storeUnsub = null;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createExpiryFromDuration } from '@novasamatech/sdk-statement';
|
|
2
|
-
import { ok, okAsync } from 'neverthrow';
|
|
2
|
+
import { ResultAsync, errAsync, ok, okAsync } from 'neverthrow';
|
|
3
3
|
import { Bytes, str } from 'scale-ts';
|
|
4
4
|
import { describe, expect, it, vi } from 'vitest';
|
|
5
5
|
import { createAccountId, createLocalSessionAccount, createRemoteSessionAccount } from '../model/sessionAccount.js';
|
|
@@ -545,4 +545,94 @@ describe('session', () => {
|
|
|
545
545
|
expect(result.unwrapOr({ responseCode: 'unknown' }).responseCode).toBe('success');
|
|
546
546
|
});
|
|
547
547
|
});
|
|
548
|
+
describe('clearOutgoingStatement', () => {
|
|
549
|
+
it('is a no-op when there is no outgoing request', async () => {
|
|
550
|
+
const { session, adapter } = makeSession();
|
|
551
|
+
await delay();
|
|
552
|
+
const before = adapter.submitStatement.mock.calls.length;
|
|
553
|
+
const result = await session.clearOutgoingStatement();
|
|
554
|
+
expect(result.isOk()).toBe(true);
|
|
555
|
+
expect(adapter.submitStatement.mock.calls.length).toBe(before);
|
|
556
|
+
});
|
|
557
|
+
it('submits an empty request batch on the same channel at >= the live expiry and clears state', async () => {
|
|
558
|
+
const { session, adapter } = makeSession();
|
|
559
|
+
await delay();
|
|
560
|
+
const rawCodec = Bytes();
|
|
561
|
+
void session.submitRequestMessage(rawCodec, new Uint8Array([1, 2, 3]));
|
|
562
|
+
await delay();
|
|
563
|
+
const liveCall = adapter.submitStatement.mock.calls.at(-1)?.[0];
|
|
564
|
+
const liveDecoded = StatementData.dec(liveCall.data);
|
|
565
|
+
expect(liveDecoded.tag).toBe('request');
|
|
566
|
+
if (liveDecoded.tag === 'request')
|
|
567
|
+
expect(liveDecoded.value.data.length).toBe(1);
|
|
568
|
+
const result = await session.clearOutgoingStatement();
|
|
569
|
+
expect(result.isOk()).toBe(true);
|
|
570
|
+
const clearCall = adapter.submitStatement.mock.calls.at(-1)?.[0];
|
|
571
|
+
const clearDecoded = StatementData.dec(clearCall.data);
|
|
572
|
+
expect(clearDecoded.tag).toBe('request');
|
|
573
|
+
if (clearDecoded.tag === 'request')
|
|
574
|
+
expect(clearDecoded.value.data).toEqual([]);
|
|
575
|
+
expect(clearCall.channel).toBe(liveCall.channel);
|
|
576
|
+
expect(clearCall.expiry).toBeGreaterThanOrEqual(liveCall.expiry);
|
|
577
|
+
// Outgoing state is cleared: the next message starts a brand-new batch (data length 1, not 2).
|
|
578
|
+
void session.submitRequestMessage(rawCodec, new Uint8Array([4]));
|
|
579
|
+
await delay();
|
|
580
|
+
const afterClear = adapter.submitStatement.mock.calls.at(-1)?.[0];
|
|
581
|
+
const afterDecoded = StatementData.dec(afterClear.data);
|
|
582
|
+
if (afterDecoded.tag === 'request')
|
|
583
|
+
expect(afterDecoded.value.data.length).toBe(1);
|
|
584
|
+
});
|
|
585
|
+
it('rejects the pending response waiter so callers unwind', async () => {
|
|
586
|
+
const { session } = makeSession();
|
|
587
|
+
await delay();
|
|
588
|
+
const submit = await session.submitRequestMessage(Bytes(), new Uint8Array([9]));
|
|
589
|
+
expect(submit.isOk()).toBe(true);
|
|
590
|
+
const requestId = submit._unsafeUnwrap().requestId;
|
|
591
|
+
const waiter = session.waitForResponseMessage(requestId);
|
|
592
|
+
await session.clearOutgoingStatement();
|
|
593
|
+
const waited = await waiter;
|
|
594
|
+
expect(waited.isErr()).toBe(true);
|
|
595
|
+
});
|
|
596
|
+
it('clears local state and rejects waiters even when the superseding submission fails', async () => {
|
|
597
|
+
const { session, adapter } = makeSession();
|
|
598
|
+
await delay();
|
|
599
|
+
const rawCodec = Bytes();
|
|
600
|
+
const submit = await session.submitRequestMessage(rawCodec, new Uint8Array([1, 2, 3]));
|
|
601
|
+
const requestId = submit._unsafeUnwrap().requestId;
|
|
602
|
+
const waiter = session.waitForResponseMessage(requestId);
|
|
603
|
+
adapter.submitStatement.mockReturnValueOnce(errAsync(new Error('store rejected')));
|
|
604
|
+
const result = await session.clearOutgoingStatement();
|
|
605
|
+
expect(result.isErr()).toBe(true);
|
|
606
|
+
// The pending waiter is rejected despite the failed submission.
|
|
607
|
+
const waited = await waiter;
|
|
608
|
+
expect(waited.isErr()).toBe(true);
|
|
609
|
+
// Local state is cleared: the next message starts a brand-new batch (data length 1, not 2).
|
|
610
|
+
adapter.submitStatement.mockReturnValue(okAsync(undefined));
|
|
611
|
+
void session.submitRequestMessage(rawCodec, new Uint8Array([4]));
|
|
612
|
+
await delay();
|
|
613
|
+
const afterClear = adapter.submitStatement.mock.calls.at(-1)?.[0];
|
|
614
|
+
const afterDecoded = StatementData.dec(afterClear.data);
|
|
615
|
+
if (afterDecoded.tag === 'request')
|
|
616
|
+
expect(afterDecoded.value.data.length).toBe(1);
|
|
617
|
+
});
|
|
618
|
+
it('cancels messages queued before the batch is submitted (init still pending)', async () => {
|
|
619
|
+
// queryStatements never resolves, so init() stays pending and the message
|
|
620
|
+
// sits in the queue with outgoingRequest still null.
|
|
621
|
+
const neverResolves = vi
|
|
622
|
+
.fn()
|
|
623
|
+
.mockReturnValue(new ResultAsync(new Promise(() => undefined)));
|
|
624
|
+
const { session, adapter } = makeSession({ queryStatements: neverResolves });
|
|
625
|
+
const submit = await session.submitRequestMessage(Bytes(), new Uint8Array([7]));
|
|
626
|
+
const requestId = submit._unsafeUnwrap().requestId;
|
|
627
|
+
const waiter = session.waitForResponseMessage(requestId);
|
|
628
|
+
const submitsBefore = adapter.submitStatement.mock.calls.length;
|
|
629
|
+
const result = await session.clearOutgoingStatement();
|
|
630
|
+
expect(result.isOk()).toBe(true);
|
|
631
|
+
// The queued waiter is rejected rather than left to be submitted after init.
|
|
632
|
+
const waited = await waiter;
|
|
633
|
+
expect(waited.isErr()).toBe(true);
|
|
634
|
+
// No empty batch is submitted since there was no live on-chain request yet.
|
|
635
|
+
expect(adapter.submitStatement.mock.calls.length).toBe(submitsBefore);
|
|
636
|
+
});
|
|
637
|
+
});
|
|
548
638
|
});
|
package/dist/session/types.d.ts
CHANGED
|
@@ -31,6 +31,15 @@ export type Session = {
|
|
|
31
31
|
requestId: string;
|
|
32
32
|
}, Error>;
|
|
33
33
|
submitResponseMessage(requestId: string, responseCode: ResponseStatus): ResultAsync<void, Error>;
|
|
34
|
+
/**
|
|
35
|
+
* Replace the in-flight outgoing request batch with an empty one on the same
|
|
36
|
+
* request channel at the session's current expiry (the statement store keeps
|
|
37
|
+
* one statement per channel and rejects only a LOWER expiry, so an equal/higher
|
|
38
|
+
* expiry supersedes the live batch). Local outgoing state is always dropped and
|
|
39
|
+
* all pending response waiters are rejected, including queued messages that have
|
|
40
|
+
* not yet been submitted and even if the superseding submission itself fails.
|
|
41
|
+
*/
|
|
42
|
+
clearOutgoingStatement(): ResultAsync<void, Error>;
|
|
34
43
|
waitForRequestMessage<T, S>(codec: Codec<T>, filter: Filter<T, S>): ResultAsync<S, Error>;
|
|
35
44
|
waitForResponseMessage(requestId: string): ResultAsync<ResponseMessage, Error>;
|
|
36
45
|
subscribe<T>(codec: Codec<T>, callback: Callback<Message<T>[]>): VoidFunction;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Ed25519-expanded sr25519 secrets (scure HDKD / `createSr25519Secret`). */
|
|
2
|
+
export declare function ensureSubstrateSr25519Ready(): void;
|
|
3
|
+
export declare function substrateSr25519PublicKey(secret: Uint8Array): Uint8Array;
|
|
4
|
+
export declare function substrateSr25519Sign(secret: Uint8Array, message: Uint8Array): Uint8Array;
|
|
5
|
+
export declare function substrateSr25519Verify(message: Uint8Array, signature: Uint8Array, publicKey: Uint8Array): boolean;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import * as schnorrkelWasm from '@polkadot-labs/schnorrkel-wasm';
|
|
2
|
+
const { sr25519_pubkey, sr25519_sign, sr25519_verify } = schnorrkelWasm;
|
|
3
|
+
/** Published .d.ts omits `init`; it is exported from the package entry at runtime. */
|
|
4
|
+
const initSchnorrkelWasm = schnorrkelWasm.init;
|
|
5
|
+
let initialized = false;
|
|
6
|
+
/** Ed25519-expanded sr25519 secrets (scure HDKD / `createSr25519Secret`). */
|
|
7
|
+
export function ensureSubstrateSr25519Ready() {
|
|
8
|
+
if (!initialized) {
|
|
9
|
+
initSchnorrkelWasm();
|
|
10
|
+
initialized = true;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export function substrateSr25519PublicKey(secret) {
|
|
14
|
+
ensureSubstrateSr25519Ready();
|
|
15
|
+
return sr25519_pubkey(secret);
|
|
16
|
+
}
|
|
17
|
+
export function substrateSr25519Sign(secret, message) {
|
|
18
|
+
ensureSubstrateSr25519Ready();
|
|
19
|
+
const publicKey = sr25519_pubkey(secret);
|
|
20
|
+
return sr25519_sign(publicKey, secret, message);
|
|
21
|
+
}
|
|
22
|
+
export function substrateSr25519Verify(message, signature, publicKey) {
|
|
23
|
+
ensureSubstrateSr25519Ready();
|
|
24
|
+
return sr25519_verify(publicKey, message, signature);
|
|
25
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@novasamatech/statement-store",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.8.0
|
|
4
|
+
"version": "0.8.0",
|
|
5
5
|
"description": "Statement store integration",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"repository": {
|
|
@@ -25,16 +25,17 @@
|
|
|
25
25
|
"README.md"
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@novasamatech/scale": "0.8.0
|
|
28
|
+
"@novasamatech/scale": "0.8.0",
|
|
29
29
|
"@novasamatech/sdk-statement": "^0.6.0",
|
|
30
30
|
"@polkadot-api/substrate-bindings": "^0.20.2",
|
|
31
31
|
"@polkadot-api/substrate-client": "^0.7.0",
|
|
32
32
|
"@polkadot-labs/hdkd-helpers": "^0.0.30",
|
|
33
|
+
"@polkadot-labs/schnorrkel-wasm": "0.0.8",
|
|
33
34
|
"@noble/hashes": "2.2.0",
|
|
34
35
|
"@noble/ciphers": "2.2.0",
|
|
35
36
|
"@scure/sr25519": "2.2.0",
|
|
36
37
|
"polkadot-api": ">=2",
|
|
37
|
-
"nanoid": "5.1.
|
|
38
|
+
"nanoid": "5.1.11",
|
|
38
39
|
"neverthrow": "^8.2.0",
|
|
39
40
|
"scale-ts": "1.6.1"
|
|
40
41
|
},
|