@novasamatech/statement-store 0.8.11 → 0.9.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.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { blake2b } from '@noble/hashes/blake2.js';
2
+ import { Bytes } from '@novasamatech/scale';
2
3
  import { deriveSlotAccountPublicKey as deriveSlotPublicKey, ensureSubstrateSlotSr25519Ready, signSlotAccountSecret as signSlotSecret, verifySlotAccountSignature as verifySlotSignature, } from '@novasamatech/substrate-slot-sr25519-wasm';
3
4
  import { entropyToMiniSecret } from '@polkadot-labs/hdkd-helpers';
4
5
  import { HDKD as sr25519HDKD, secretFromSeed as sr25519SecretFromSeed } from '@scure/sr25519';
5
- import { Bytes, str, u64 } from 'scale-ts';
6
+ import { str, u64 } from 'scale-ts';
6
7
  import { substrateSr25519PublicKey, substrateSr25519Sign, substrateSr25519Verify } from './substrateSr25519.js';
7
8
  export { ensureSubstrateSlotSr25519Ready };
8
9
  export { ensureSubstrateSr25519Ready } from './substrateSr25519.js';
@@ -1,5 +1,5 @@
1
1
  import type { Branded } from '../types.js';
2
2
  import type { SessionAccount } from './sessionAccount.js';
3
3
  export type SessionId = Branded<Uint8Array, 'SessionId'>;
4
- export declare const SessionIdCodec: import("scale-ts").Codec<Uint8Array<ArrayBufferLike>>;
4
+ export declare const SessionIdCodec: import("@novasamatech/scale").BytesCodec<number>;
5
5
  export declare function createSessionId(sharedSecret: Uint8Array, accountA: SessionAccount, accountB: SessionAccount): SessionId;
@@ -1,5 +1,5 @@
1
+ import { Bytes } from '@novasamatech/scale';
1
2
  import { mergeUint8 } from 'polkadot-api/utils';
2
- import { Bytes } from 'scale-ts';
3
3
  import { khash, stringToBytes } from '../crypto.js';
4
4
  export const SessionIdCodec = Bytes(32);
5
5
  export function createSessionId(sharedSecret, accountA, accountB) {
@@ -1,4 +1,5 @@
1
- import { Bytes, Option, Struct, str } from 'scale-ts';
1
+ import { Bytes } from '@novasamatech/scale';
2
+ import { Option, Struct, str } from 'scale-ts';
2
3
  import { BrandedBytesCodec } from '../crypto.js';
3
4
  export const AccountIdCodec = BrandedBytesCodec(32);
4
5
  export function createAccountId(value) {
@@ -1,24 +1,24 @@
1
- import { gcm } from '@noble/ciphers/aes.js';
1
+ import { chacha20poly1305 } from '@noble/ciphers/chacha.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
5
  import { Result, fromThrowable } from 'neverthrow';
6
6
  import { mergeUint8 } from 'polkadot-api/utils';
7
7
  export function createEncryption(sharedSecret) {
8
- const salt = new Uint8Array(); // secure enough since P256 random keys provide enough entropy
8
+ const salt = new Uint8Array(); // secure enough since the X25519 shared secret provides full entropy
9
9
  const info = new Uint8Array(); // no need to introduce any context
10
- const aesKey = hkdf(sha256, sharedSecret, salt, info, 32);
10
+ const aeadKey = hkdf(sha256, sharedSecret, salt, info, 32);
11
11
  return {
12
12
  encrypt: fromThrowable(cipherText => {
13
13
  const nonce = randomBytes(12);
14
- const aes = gcm(aesKey, nonce);
15
- return mergeUint8([nonce, aes.encrypt(cipherText)]);
14
+ const aead = chacha20poly1305(aeadKey, nonce);
15
+ return mergeUint8([nonce, aead.encrypt(cipherText)]);
16
16
  }),
17
17
  decrypt: fromThrowable(encryptedMessage => {
18
18
  const nonce = encryptedMessage.slice(0, 12);
19
19
  const cipherText = encryptedMessage.slice(12);
20
- const aes = gcm(aesKey, nonce);
21
- return aes.decrypt(cipherText);
20
+ const aead = chacha20poly1305(aeadKey, nonce);
21
+ return aead.decrypt(cipherText);
22
22
  }),
23
23
  };
24
24
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,43 @@
1
+ import { chacha20poly1305 } from '@noble/ciphers/chacha.js';
2
+ import { hkdf } from '@noble/hashes/hkdf.js';
3
+ import { sha256 } from '@noble/hashes/sha2.js';
4
+ import { randomBytes } from '@noble/hashes/utils.js';
5
+ import { describe, expect, it } from 'vitest';
6
+ import { createEncryption } from './encyption.js';
7
+ const deriveAeadKey = (sharedSecret) => hkdf(sha256, sharedSecret, new Uint8Array(), new Uint8Array(), 32);
8
+ describe('statement-store message encryption', () => {
9
+ it('round-trips a message', () => {
10
+ const sharedSecret = randomBytes(32);
11
+ const encryption = createEncryption(sharedSecret);
12
+ const plaintext = new TextEncoder().encode('hello statement');
13
+ const encrypted = encryption.encrypt(plaintext)._unsafeUnwrap();
14
+ const decrypted = encryption.decrypt(encrypted)._unsafeUnwrap();
15
+ expect(decrypted).toEqual(plaintext);
16
+ });
17
+ it('produces ChaCha20-Poly1305 ciphertext decryptable by an external ChaCha20-Poly1305 reader', () => {
18
+ const sharedSecret = randomBytes(32);
19
+ const plaintext = new TextEncoder().encode('cross-decrypt me');
20
+ const encrypted = createEncryption(sharedSecret).encrypt(plaintext)._unsafeUnwrap();
21
+ const nonce = encrypted.slice(0, 12);
22
+ const body = encrypted.slice(12);
23
+ const aeadKey = deriveAeadKey(sharedSecret);
24
+ const decrypted = chacha20poly1305(aeadKey, nonce).decrypt(body);
25
+ expect(decrypted).toEqual(plaintext);
26
+ });
27
+ it('decrypts a ChaCha20-Poly1305 ciphertext built externally (nonce || ct || tag framing)', () => {
28
+ const sharedSecret = randomBytes(32);
29
+ const plaintext = new TextEncoder().encode('external chacha');
30
+ const aeadKey = deriveAeadKey(sharedSecret);
31
+ const nonce = randomBytes(12);
32
+ const body = chacha20poly1305(aeadKey, nonce).encrypt(plaintext);
33
+ const wire = new Uint8Array([...nonce, ...body]);
34
+ const decrypted = createEncryption(sharedSecret).decrypt(wire)._unsafeUnwrap();
35
+ expect(decrypted).toEqual(plaintext);
36
+ });
37
+ it('fails to decrypt with a different shared secret', () => {
38
+ const plaintext = new TextEncoder().encode('secret');
39
+ const encrypted = createEncryption(randomBytes(32)).encrypt(plaintext)._unsafeUnwrap();
40
+ const result = createEncryption(randomBytes(32)).decrypt(encrypted);
41
+ expect(result.isErr()).toBe(true);
42
+ });
43
+ });
@@ -1,4 +1,5 @@
1
- import { Bytes, Enum, Struct, Vector, enhanceCodec, str, u8 } from 'scale-ts';
1
+ import { Bytes } from '@novasamatech/scale';
2
+ import { Enum, Struct, Vector, enhanceCodec, str, u8 } from 'scale-ts';
2
3
  export const ResponseCode = enhanceCodec(u8, error => {
3
4
  switch (error) {
4
5
  case 'success':
@@ -1,6 +1,7 @@
1
+ import { Bytes } from '@novasamatech/scale';
1
2
  import { createExpiryFromDuration } from '@novasamatech/sdk-statement';
2
3
  import { ResultAsync, err, errAsync, ok, okAsync } from 'neverthrow';
3
- import { Bytes, Struct, str } from 'scale-ts';
4
+ import { Struct, str } from 'scale-ts';
4
5
  import { describe, expect, it, vi } from 'vitest';
5
6
  import { createInMemoryStatementStore } from '../adapter/inMemory.js';
6
7
  import { AccountFullError, ExpiryTooLowError } from '../adapter/types.js';
@@ -263,7 +264,7 @@ describe('session', () => {
263
264
  const { session } = makeSession({ peer: [peerRequest] });
264
265
  await delay();
265
266
  const result = await session.submitResponseMessage(requestId, 'success');
266
- expect(result.isOk()).toBe(true);
267
+ await expect(result).toBeOk();
267
268
  });
268
269
  it('treats an incoming request as already answered when our response is present (no resubmit)', async () => {
269
270
  const requestId = 'peer-request-id';
@@ -274,7 +275,7 @@ describe('session', () => {
274
275
  await delay();
275
276
  const submitsBefore = adapter.submitStatement.mock.calls.length;
276
277
  const result = await session.submitResponseMessage(requestId, 'success');
277
- expect(result.isOk()).toBe(true);
278
+ await expect(result).toBeOk();
278
279
  expect(adapter.submitStatement.mock.calls.length).toBe(submitsBefore); // no new submit
279
280
  });
280
281
  it('does not re-deliver an incoming request we already answered', async () => {
@@ -365,8 +366,7 @@ describe('session', () => {
365
366
  });
366
367
  await delay();
367
368
  const result = await responsePromise;
368
- expect(result.isOk()).toBe(true);
369
- expect(result.unwrapOr({ responseCode: 'unknown' }).responseCode).toBe('success');
369
+ await expect(result).toBeOkWith(expect.objectContaining({ responseCode: 'success' }));
370
370
  });
371
371
  it('does not resend a message that is already in flight (dedup)', async () => {
372
372
  const store = createInMemoryStatementStore();
@@ -401,8 +401,8 @@ describe('session', () => {
401
401
  isComplete: true,
402
402
  });
403
403
  await delay();
404
- expect((await w1).isOk()).toBe(true);
405
- expect((await w2).isOk()).toBe(true);
404
+ await expect(w1).toBeOk();
405
+ await expect(w2).toBeOk();
406
406
  });
407
407
  it('preserves FIFO order: a later fitting message does not overtake queued ones', async () => {
408
408
  const m1 = rawCodec.enc(new Uint8Array([1, 2, 3])); // first → in-flight batch
@@ -426,7 +426,7 @@ describe('session', () => {
426
426
  await delay();
427
427
  adapter.submitStatement.mockClear();
428
428
  const result = await session.submitRequestMessage(rawCodec, new Uint8Array([1, 2, 3, 4]));
429
- expect(result.isErr()).toBe(true);
429
+ await expect(result).toBeErr();
430
430
  expect(adapter.submitStatement).not.toHaveBeenCalled();
431
431
  });
432
432
  });
@@ -524,7 +524,7 @@ describe('session', () => {
524
524
  const { session } = makeSession({ peer: [peerRequest] });
525
525
  await delay(); // init buffers the peer request
526
526
  const result = await session.waitForRequestMessage(rawCodec, () => 'matched');
527
- expect(result._unsafeUnwrap()).toBe('matched');
527
+ await expect(result).toBeOkWith('matched');
528
528
  }, 2000);
529
529
  it('opens a single subscription on the incoming topic', () => {
530
530
  const store = createInMemoryStatementStore();
@@ -591,7 +591,7 @@ describe('session', () => {
591
591
  const { session } = makeSession();
592
592
  await delay();
593
593
  const result = await session.submitResponseMessage('wrong-id', 'success');
594
- expect(result.isErr()).toBe(true);
594
+ await expect(result).toBeErr();
595
595
  });
596
596
  it('NACKs an undecodable incoming request with decodingFailed', async () => {
597
597
  // A request (enum tag 0) whose requestId decodes but whose data vector claims more
@@ -650,7 +650,7 @@ describe('session', () => {
650
650
  expect(adapter.submitStatement).not.toHaveBeenCalled(); // no premature NACK
651
651
  // The legitimate response still goes through.
652
652
  const res = await session.submitResponseMessage(reqId, 'success');
653
- expect(res.isOk()).toBe(true);
653
+ await expect(res).toBeOk();
654
654
  const decoded = StatementData.dec(lastSubmitted(adapter).data);
655
655
  expect(decoded.tag === 'response' && decoded.value.responseCode).toBe('success');
656
656
  });
@@ -676,7 +676,7 @@ describe('session', () => {
676
676
  host.respondToRequests(RemoteMsg, () => 'success');
677
677
  await settle();
678
678
  const ack = await peer.request(RemoteMsg, requestMsg('p1'));
679
- expect(ack.isOk()).toBe(true);
679
+ await expect(ack).toBeOk();
680
680
  host.dispose();
681
681
  peer.dispose();
682
682
  });
@@ -688,7 +688,7 @@ describe('session', () => {
688
688
  host.respondToRequests(RemoteMsg, () => okAsync('decodingFailed'));
689
689
  await settle();
690
690
  const ack = await peer.request(RemoteMsg, requestMsg('p1'));
691
- expect(ack.isErr()).toBe(true);
691
+ await expect(ack).toBeErr();
692
692
  expect(ack._unsafeUnwrapErr()).toBeInstanceOf(DecodingError);
693
693
  host.dispose();
694
694
  peer.dispose();
@@ -701,7 +701,7 @@ describe('session', () => {
701
701
  host.respondToRequests(RemoteMsg, () => errAsync(new Error('handler boom')));
702
702
  await settle();
703
703
  const ack = await peer.request(RemoteMsg, requestMsg('p1'));
704
- expect(ack.isErr()).toBe(true);
704
+ await expect(ack).toBeErr();
705
705
  expect(ack._unsafeUnwrapErr()).toBeInstanceOf(UnknownError);
706
706
  host.dispose();
707
707
  peer.dispose();
@@ -731,8 +731,8 @@ describe('session', () => {
731
731
  await delay();
732
732
  const resA = await session.submitResponseMessage('A', 'success');
733
733
  const resB = await session.submitResponseMessage('B', 'success');
734
- expect(resA.isOk()).toBe(true);
735
- expect(resB.isOk()).toBe(true);
734
+ await expect(resA).toBeOk();
735
+ await expect(resB).toBeOk();
736
736
  });
737
737
  it('remains answerable after response submission retries are exhausted', async () => {
738
738
  const peerRequest = makeStatement({ tag: 'request', value: { requestId: 'rid', data: [] } });
@@ -740,11 +740,11 @@ describe('session', () => {
740
740
  await delay();
741
741
  adapter.submitStatement.mockReturnValue(errAsync(new Error('store rejected')));
742
742
  const first = await session.submitResponseMessage('rid', 'success'); // all retries fail → err + rollback
743
- expect(first.isErr()).toBe(true);
743
+ await expect(first).toBeErr();
744
744
  adapter.submitStatement.mockReturnValue(okAsync(undefined)); // store recovers
745
745
  const submitsBefore = adapter.submitStatement.mock.calls.length;
746
746
  const second = await session.submitResponseMessage('rid', 'success'); // retryable → submits and succeeds
747
- expect(second.isOk()).toBe(true);
747
+ await expect(second).toBeOk();
748
748
  expect(adapter.submitStatement.mock.calls.length).toBeGreaterThan(submitsBefore);
749
749
  }, 3000);
750
750
  it.each([
@@ -774,13 +774,13 @@ describe('session', () => {
774
774
  pendings.find(p => p.requestId === 'A').settle(err(new PriorityError(0n, 0n))); // A lands late, rejected
775
775
  const resA = await resAPromise;
776
776
  const resB = await resBPromise;
777
- expect(resB.isOk()).toBe(true);
778
- expect(resA.isOk()).toBe(true); // superseded rejection absorbed, not surfaced as an error
777
+ await expect(resB).toBeOk();
778
+ await expect(resA).toBeOk(); // superseded rejection absorbed, not surfaced as an error
779
779
  // A stays answered: re-answering it must NOT submit again (which would clobber B's response).
780
780
  const submitsBefore = adapter.submitStatement.mock.calls.length;
781
781
  const reAnswer = await session.submitResponseMessage('A', 'success');
782
782
  await delay();
783
- expect(reAnswer.isOk()).toBe(true);
783
+ await expect(reAnswer).toBeOk();
784
784
  expect(adapter.submitStatement.mock.calls.length).toBe(submitsBefore); // deduped → no resubmit
785
785
  session.dispose();
786
786
  }, 3000);
@@ -793,7 +793,7 @@ describe('session', () => {
793
793
  await delay();
794
794
  const before = adapter.submitStatement.mock.calls.length;
795
795
  const result = await session.clearOutgoingStatement();
796
- expect(result.isOk()).toBe(true);
796
+ await expect(result).toBeOk();
797
797
  expect(adapter.submitStatement.mock.calls.length).toBe(before);
798
798
  });
799
799
  it('absorbs an ExpiryTooLow on the superseding empty batch as success', async () => {
@@ -809,7 +809,7 @@ describe('session', () => {
809
809
  void session.submitRequestMessage(rawCodec, new Uint8Array([1]));
810
810
  await delay();
811
811
  const cleared = await session.clearOutgoingStatement();
812
- expect(cleared.isOk()).toBe(true); // ExpiryTooLow suppressed
812
+ await expect(cleared).toBeOk(); // ExpiryTooLow suppressed
813
813
  session.dispose();
814
814
  }, 3000);
815
815
  it('submits an empty batch on the same channel at a higher expiry and clears local state', async () => {
@@ -823,7 +823,7 @@ describe('session', () => {
823
823
  if (liveDecoded.tag === 'request')
824
824
  expect(liveDecoded.value.data.length).toBe(1);
825
825
  const result = await session.clearOutgoingStatement();
826
- expect(result.isOk()).toBe(true);
826
+ await expect(result).toBeOk();
827
827
  const clearCall = adapter.submitStatement.mock.calls.at(-1)?.[0];
828
828
  const clearDecoded = StatementData.dec(clearCall.data);
829
829
  expect(clearDecoded.tag).toBe('request');
@@ -848,7 +848,7 @@ describe('session', () => {
848
848
  void session.submitRequestMessage(rawCodec, new Uint8Array([1, 2, 3]));
849
849
  await settle();
850
850
  const result = await session.clearOutgoingStatement();
851
- expect(result.isOk()).toBe(true);
851
+ await expect(result).toBeOk();
852
852
  await settle();
853
853
  const requests = store
854
854
  .currentStatements()
@@ -862,12 +862,12 @@ describe('session', () => {
862
862
  const { session } = makeSession();
863
863
  await delay();
864
864
  const submit = await session.submitRequestMessage(rawCodec, new Uint8Array([9]));
865
- expect(submit.isOk()).toBe(true);
865
+ await expect(submit).toBeOk();
866
866
  const requestId = submit._unsafeUnwrap().requestId;
867
867
  const waiter = session.waitForResponseMessage(requestId);
868
868
  await session.clearOutgoingStatement();
869
869
  const waited = await waiter;
870
- expect(waited.isErr()).toBe(true);
870
+ await expect(waited).toBeErr();
871
871
  });
872
872
  it('clears local state and rejects waiters even when the supersede submission fails', async () => {
873
873
  const { session, adapter } = makeSession();
@@ -877,10 +877,10 @@ describe('session', () => {
877
877
  const waiter = session.waitForResponseMessage(requestId);
878
878
  adapter.submitStatement.mockReturnValueOnce(errAsync(new Error('store rejected')));
879
879
  const result = await session.clearOutgoingStatement();
880
- expect(result.isErr()).toBe(true);
880
+ await expect(result).toBeErr();
881
881
  // The pending waiter is rejected despite the failed submission.
882
882
  const waited = await waiter;
883
- expect(waited.isErr()).toBe(true);
883
+ await expect(waited).toBeErr();
884
884
  // State is cleared: the next message starts a brand-new batch (data length 1, not 2).
885
885
  adapter.submitStatement.mockReturnValue(okAsync(undefined));
886
886
  void session.submitRequestMessage(rawCodec, new Uint8Array([4]));
@@ -902,10 +902,10 @@ describe('session', () => {
902
902
  const waiter = session.waitForResponseMessage(requestId);
903
903
  const submitsBefore = adapter.submitStatement.mock.calls.length;
904
904
  const result = await session.clearOutgoingStatement();
905
- expect(result.isOk()).toBe(true);
905
+ await expect(result).toBeOk();
906
906
  // The queued waiter is rejected rather than left to be submitted after init.
907
907
  const waited = await waiter;
908
- expect(waited.isErr()).toBe(true);
908
+ await expect(waited).toBeErr();
909
909
  // No empty batch is submitted since there was no live on-chain request yet.
910
910
  expect(adapter.submitStatement.mock.calls.length).toBe(submitsBefore);
911
911
  });
@@ -1011,7 +1011,7 @@ describe('session', () => {
1011
1011
  const submit = await session.submitRequestMessage(rawCodec, new Uint8Array([1]));
1012
1012
  const requestId = submit._unsafeUnwrap().requestId;
1013
1013
  const waited = await session.waitForResponseMessage(requestId);
1014
- expect(waited.isErr()).toBe(true);
1014
+ await expect(waited).toBeErr();
1015
1015
  }, 2000);
1016
1016
  it('absorbs a superseded older submission rejected as ExpiryTooLow without surfacing an error', async () => {
1017
1017
  // Two messages batch onto one outgoing request: the first submission (requestId A) is in
@@ -1055,7 +1055,7 @@ describe('session', () => {
1055
1055
  const waiter = session.waitForRequestMessage(rawCodec, () => 'x');
1056
1056
  session.dispose();
1057
1057
  const result = await waiter;
1058
- expect(result.isErr()).toBe(true);
1058
+ await expect(result).toBeErr();
1059
1059
  }, 2000);
1060
1060
  it('cancels a pending init retry (no further queries)', async () => {
1061
1061
  const queryStatements = vi.fn(() => errAsync(new Error('store down'))); // init always fails → schedules retry
@@ -1071,7 +1071,7 @@ describe('session', () => {
1071
1071
  await delay();
1072
1072
  session.dispose();
1073
1073
  const result = await session.submitRequestMessage(rawCodec, new Uint8Array([1]));
1074
- expect(result.isErr()).toBe(true); // surfaced immediately, not a token left pending forever
1074
+ await expect(result).toBeErr(); // surfaced immediately, not a token left pending forever
1075
1075
  expect(adapter.submitStatement).not.toHaveBeenCalled();
1076
1076
  });
1077
1077
  it('rejects submitResponseMessage after dispose', async () => {
@@ -1079,7 +1079,7 @@ describe('session', () => {
1079
1079
  await delay();
1080
1080
  session.dispose();
1081
1081
  const result = await session.submitResponseMessage('any-id', 'success');
1082
- expect(result.isErr()).toBe(true);
1082
+ await expect(result).toBeErr();
1083
1083
  });
1084
1084
  it('does not re-activate when disposed while init is in flight', async () => {
1085
1085
  // dispose() lands during init's query await; init must bail before restoring state / flipping
@@ -1089,13 +1089,13 @@ describe('session', () => {
1089
1089
  const queryStatements = vi.fn(() => ResultAsync.fromSafePromise(gate));
1090
1090
  const { session, adapter } = makeSession({ queryStatements });
1091
1091
  const queued = await session.submitRequestMessage(rawCodec, new Uint8Array([1])); // queued during init
1092
- expect(queued.isOk()).toBe(true);
1092
+ await expect(queued).toBeOk();
1093
1093
  session.dispose(); // dispose mid-init
1094
1094
  resolveQueries([]); // init resumes — must bail before draining the queue / activating
1095
1095
  await settle();
1096
1096
  expect(adapter.submitStatement).not.toHaveBeenCalled(); // no resurrection-driven submit
1097
1097
  const after = await session.submitRequestMessage(rawCodec, new Uint8Array([2]));
1098
- expect(after.isErr()).toBe(true); // session stays disposed
1098
+ await expect(after).toBeErr(); // session stays disposed
1099
1099
  }, 3000);
1100
1100
  });
1101
1101
  // The in-memory adapter replicates the store's observable contract; `fidelity` pins the double's
@@ -1123,7 +1123,7 @@ describe('session', () => {
1123
1123
  const store = createInMemoryStatementStore();
1124
1124
  await store.submitStatement(makeSignedStatement(hex(0xaa), 10n, hex(0x01), new Uint8Array([1])));
1125
1125
  const higher = await store.submitStatement(makeSignedStatement(hex(0xaa), 11n, hex(0x01), new Uint8Array([2])));
1126
- expect(higher.isOk()).toBe(true);
1126
+ await expect(higher).toBeOk();
1127
1127
  expect(store.currentStatements()).toHaveLength(1);
1128
1128
  expect(store.currentStatements()[0]?.expiry).toBe(11n);
1129
1129
  });
@@ -1132,9 +1132,9 @@ describe('session', () => {
1132
1132
  await store.submitStatement(makeSignedStatement(hex(0xaa), 10n, hex(0x01), new Uint8Array([1])));
1133
1133
  const equal = await store.submitStatement(makeSignedStatement(hex(0xaa), 10n, hex(0x01), new Uint8Array([9])));
1134
1134
  const lower = await store.submitStatement(makeSignedStatement(hex(0xaa), 5n, hex(0x01), new Uint8Array([9])));
1135
- expect(equal.isErr()).toBe(true);
1135
+ await expect(equal).toBeErr();
1136
1136
  expect(equal._unsafeUnwrapErr()).toBeInstanceOf(ExpiryTooLowError);
1137
- expect(lower.isErr()).toBe(true);
1137
+ await expect(lower).toBeErr();
1138
1138
  // The original statement is untouched.
1139
1139
  expect(store.currentStatements()[0]?.data).toEqual(new Uint8Array([1]));
1140
1140
  });
@@ -1143,7 +1143,7 @@ describe('session', () => {
1143
1143
  const stmt = makeSignedStatement(hex(0xaa), 10n, hex(0x01), new Uint8Array([1]));
1144
1144
  await store.submitStatement(stmt);
1145
1145
  const again = await store.submitStatement(stmt);
1146
- expect(again.isOk()).toBe(true);
1146
+ await expect(again).toBeOk();
1147
1147
  expect(store.currentStatements()).toHaveLength(1);
1148
1148
  });
1149
1149
  it('coexists statements on different channels sharing a topic', async () => {
@@ -1184,9 +1184,9 @@ describe('session', () => {
1184
1184
  const mobileReplyAck = mobileGotRequest.andThen(req => mobile.request(RemoteMsg, { id: 'm1', kind: 'reply', respondingTo: req.id, body: 'signature' }));
1185
1185
  const hostReply = host.waitForRequestMessage(RemoteMsg, msg => msg.kind === 'reply' && msg.respondingTo === 'h1' ? msg.body : undefined);
1186
1186
  await settle();
1187
- expect((await hostAck).isOk()).toBe(true); // mobile ACKed the host request
1188
- expect((await mobileReplyAck).isOk()).toBe(true); // host ACKed the mobile reply
1189
- expect((await hostReply)._unsafeUnwrap()).toBe('signature'); // host received the reply
1187
+ await expect(hostAck).toBeOk(); // mobile ACKed the host request
1188
+ await expect(mobileReplyAck).toBeOk(); // host ACKed the mobile reply
1189
+ await expect(hostReply).toBeOkWith('signature'); // host received the reply
1190
1190
  host.dispose();
1191
1191
  mobile.dispose();
1192
1192
  });
@@ -1212,7 +1212,7 @@ describe('session', () => {
1212
1212
  mobile = makeMobile(store);
1213
1213
  mobile.respondToRequests(RemoteMsg, () => 'success');
1214
1214
  await settle();
1215
- expect((await hostAck).isOk()).toBe(true);
1215
+ await expect(hostAck).toBeOk();
1216
1216
  host.dispose();
1217
1217
  mobile.dispose();
1218
1218
  });
@@ -1233,7 +1233,7 @@ describe('session', () => {
1233
1233
  const hostAck = host.request(RemoteMsg, { id: 'h1', kind: 'request', respondingTo: '', body: 'sign this' });
1234
1234
  await settle();
1235
1235
  // The reply is delivered…
1236
- expect((await hostReply)._unsafeUnwrap()).toBe('signature');
1236
+ await expect(hostReply).toBeOkWith('signature');
1237
1237
  // …while the transport ACK is still outstanding (mobile never sent it).
1238
1238
  const ackState = await Promise.race([
1239
1239
  Promise.resolve(hostAck).then(() => 'resolved'),
@@ -32,7 +32,7 @@ describe('statementProver', () => {
32
32
  const signed = (await prover.generateMessageProof(makeStatement(new Uint8Array([1, 2, 3]))))._unsafeUnwrap();
33
33
  expect(proofSigner(signed)).toBe(toHex(deriveSr25519PublicKey(secret)));
34
34
  const verified = await prover.verifyMessageProof(signed);
35
- expect(verified._unsafeUnwrap()).toBe(true);
35
+ await expect(verified).toBeOkWith(true);
36
36
  });
37
37
  });
38
38
  describe('createSlotAccountProver (mobile slot secrets)', () => {
@@ -44,7 +44,7 @@ describe('statementProver', () => {
44
44
  const signed = (await prover.generateMessageProof(makeStatement(new Uint8Array([4, 5, 6]))))._unsafeUnwrap();
45
45
  expect(proofSigner(signed)).toBe(toHex(deriveSlotAccountPublicKey(slotSecret)));
46
46
  const verified = await prover.verifyMessageProof(signed);
47
- expect(verified._unsafeUnwrap()).toBe(true);
47
+ await expect(verified).toBeOkWith(true);
48
48
  });
49
49
  it('signs under a different public key than the scure prover would for the same secret', () => {
50
50
  // Regression guard: a slot secret pushed through the scure scheme derives the WRONG
@@ -58,14 +58,14 @@ describe('statementProver', () => {
58
58
  const signed = (await prover.generateMessageProof(makeStatement(new Uint8Array([7, 8, 9]))))._unsafeUnwrap();
59
59
  const tampered = { ...signed, data: new Uint8Array([9, 9, 9]) };
60
60
  const verified = await prover.verifyMessageProof(tampered);
61
- expect(verified._unsafeUnwrap()).toBe(false);
61
+ await expect(verified).toBeOkWith(false);
62
62
  });
63
63
  });
64
64
  describe('verifyMessageProof', () => {
65
65
  it('errors when the statement carries no proof', async () => {
66
66
  const prover = createSr25519Prover(createSr25519Secret(mnemonicToEntropy(DEV_MNEMONIC)));
67
67
  const verified = await prover.verifyMessageProof(makeStatement(new Uint8Array([1])));
68
- expect(verified.isErr()).toBe(true);
68
+ await expect(verified).toBeErr();
69
69
  });
70
70
  });
71
71
  });
@@ -15,7 +15,7 @@ describe('submitWithRetry', () => {
15
15
  let calls = 0;
16
16
  const submit = vi.fn(() => ++calls <= 6 ? errAsync(new AccountFullError(0n, 1n)) : okAsync(undefined));
17
17
  const result = await submitWithRetry(submit, { ...FAST, attempts: 3, priorityAttempts: 'unbounded' });
18
- expect(result.isOk()).toBe(true);
18
+ await expect(result).toBeOk();
19
19
  expect(calls).toBe(7);
20
20
  });
21
21
  it("priorityAttempts 'unbounded': a no-longer-live priority rejection settles as success", async () => {
@@ -26,13 +26,13 @@ describe('submitWithRetry', () => {
26
26
  priorityAttempts: 'unbounded',
27
27
  shouldRetry: () => false,
28
28
  });
29
- expect(result.isOk()).toBe(true); // lost the channel race — benign
29
+ await expect(result).toBeOk(); // lost the channel race — benign
30
30
  expect(submit).toHaveBeenCalledTimes(1);
31
31
  });
32
32
  it('priorityAttempts budgeted: priority errors consume their budget then propagate', async () => {
33
33
  const submit = vi.fn(() => errAsync(new AccountFullError(0n, 1n)));
34
34
  const result = await submitWithRetry(submit, { ...FAST, attempts: 0, priorityAttempts: 3 });
35
- expect(result.isErr()).toBe(true);
35
+ await expect(result).toBeErr();
36
36
  expect(result._unsafeUnwrapErr()).toBeInstanceOf(AccountFullError);
37
37
  expect(submit).toHaveBeenCalledTimes(4); // 1 initial + 3 retries
38
38
  });
@@ -46,7 +46,7 @@ describe('submitWithRetry', () => {
46
46
  priorityAttempts: 2,
47
47
  onPriorityError: error => seen.push(error.min),
48
48
  });
49
- expect(result.isErr()).toBe(true);
49
+ await expect(result).toBeErr();
50
50
  expect(submit).toHaveBeenCalledTimes(3); // 1 initial + 2 retries
51
51
  expect(seen).toEqual([1n, 2n, 3n]); // adopted the floor on all three, including the terminal rejection
52
52
  });
@@ -59,13 +59,13 @@ describe('submitWithRetry', () => {
59
59
  it('attempts 0: a non-priority error propagates immediately', async () => {
60
60
  const submit = vi.fn(() => errAsync(new Error('store rejected')));
61
61
  const result = await submitWithRetry(submit, { ...FAST, attempts: 0, priorityAttempts: 3 });
62
- expect(result.isErr()).toBe(true);
62
+ await expect(result).toBeErr();
63
63
  expect(submit).toHaveBeenCalledTimes(1);
64
64
  });
65
65
  it('non-priority errors consume the attempts budget then propagate', async () => {
66
66
  const submit = vi.fn(() => errAsync(new Error('store rejected')));
67
67
  const result = await submitWithRetry(submit, { ...FAST, attempts: 2, priorityAttempts: 'unbounded' });
68
- expect(result.isErr()).toBe(true);
68
+ await expect(result).toBeErr();
69
69
  expect(submit).toHaveBeenCalledTimes(3); // 1 initial + 2 retries
70
70
  });
71
71
  it('per-attempt delay schedule is honored and reported via onRetry', async () => {
@@ -78,7 +78,7 @@ describe('submitWithRetry', () => {
78
78
  delaysMs: [1, 2, 3],
79
79
  onRetry: ({ attempt, delayMs }) => seen.push({ attempt, delayMs }),
80
80
  });
81
- expect(result.isOk()).toBe(true);
81
+ await expect(result).toBeOk();
82
82
  expect(seen).toEqual([
83
83
  { attempt: 0, delayMs: 1 },
84
84
  { attempt: 1, delayMs: 2 },
@@ -87,7 +87,7 @@ describe('submitWithRetry', () => {
87
87
  it('a negative budget propagates immediately instead of looping', async () => {
88
88
  const submit = vi.fn(() => errAsync(new Error('store rejected')));
89
89
  const result = await submitWithRetry(submit, { ...FAST, attempts: -1, priorityAttempts: 3 });
90
- expect(result.isErr()).toBe(true);
90
+ await expect(result).toBeErr();
91
91
  expect(submit).toHaveBeenCalledTimes(1);
92
92
  });
93
93
  it('a shouldRetry flip during the backoff settles a priority rejection as success', async () => {
@@ -104,7 +104,7 @@ describe('submitWithRetry', () => {
104
104
  priorityAttempts: 'unbounded',
105
105
  shouldRetry: () => live,
106
106
  });
107
- expect(result.isOk()).toBe(true); // settled after the delay, no second attempt
107
+ await expect(result).toBeOk(); // settled after the delay, no second attempt
108
108
  expect(submit).toHaveBeenCalledTimes(1);
109
109
  });
110
110
  it('shouldRetry is re-checked before each retry and stops the loop', async () => {
@@ -119,7 +119,7 @@ describe('submitWithRetry', () => {
119
119
  priorityAttempts: 'unbounded',
120
120
  shouldRetry: () => live,
121
121
  });
122
- expect(result.isErr()).toBe(true); // non-priority + not live → propagate, no settle
122
+ await expect(result).toBeErr(); // non-priority + not live → propagate, no settle
123
123
  expect(submit).toHaveBeenCalledTimes(1);
124
124
  });
125
125
  });
@@ -41,7 +41,7 @@ describe('submitStatementOnce', () => {
41
41
  it('submits with the pinned-high expiry layout', async () => {
42
42
  const { adapter, submitted } = makeStore();
43
43
  const result = await submitStatementOnce(baseParams(adapter));
44
- expect(result.isOk()).toBe(true);
44
+ await expect(result).toBeOk();
45
45
  expect(submitted).toHaveLength(1);
46
46
  expect((submitted[0].expiry ?? 0n) >> 32n).toBe(0xffffffffn);
47
47
  });
@@ -51,8 +51,8 @@ describe('submitStatementOnce', () => {
51
51
  const params = baseParams(adapter);
52
52
  const first = await submitStatementOnce(params);
53
53
  const second = await submitStatementOnce(params);
54
- expect(first.isErr()).toBe(true);
55
- expect(second.isOk()).toBe(true);
54
+ await expect(first).toBeErr();
55
+ await expect(second).toBeOk();
56
56
  expect(submitted[1].expiry ?? 0n).toBeGreaterThan(chainMin); // adopted min, not wall clock
57
57
  });
58
58
  });
@@ -73,7 +73,7 @@ describe('signAndSubmitStatement', () => {
73
73
  });
74
74
  await vi.advanceTimersByTimeAsync(600); // cover the 500ms first backoff
75
75
  const result = await promise;
76
- expect(result.isOk()).toBe(true);
76
+ await expect(result).toBeOk();
77
77
  expect(submitted).toHaveLength(2);
78
78
  expect(submitted[1].expiry ?? 0n).toBeGreaterThan(chainMin);
79
79
  });
@@ -92,7 +92,7 @@ describe('signAndSubmitStatement', () => {
92
92
  });
93
93
  await vi.advanceTimersByTimeAsync(50);
94
94
  const result = await promise;
95
- expect(result.isErr()).toBe(true);
95
+ await expect(result).toBeErr();
96
96
  expect(result._unsafeUnwrapErr()).toBeInstanceOf(AccountFullError);
97
97
  expect(submitted).toHaveLength(4); // 1 initial + 3 priority retries
98
98
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@novasamatech/statement-store",
3
3
  "type": "module",
4
- "version": "0.8.11",
4
+ "version": "0.9.0",
5
5
  "description": "Statement store integration",
6
6
  "license": "Apache-2.0",
7
7
  "repository": {
@@ -25,18 +25,18 @@
25
25
  "README.md"
26
26
  ],
27
27
  "dependencies": {
28
- "@novasamatech/scale": "0.8.11",
28
+ "@novasamatech/scale": "0.9.0",
29
29
  "@novasamatech/sdk-statement": "^0.6.0",
30
- "@novasamatech/substrate-slot-sr25519-wasm": "0.8.11",
30
+ "@novasamatech/substrate-slot-sr25519-wasm": "0.9.0",
31
31
  "@polkadot-api/substrate-bindings": "^0.20.3",
32
32
  "@polkadot-api/substrate-client": "^0.7.0",
33
- "@polkadot-labs/hdkd-helpers": "^0.0.30",
33
+ "@polkadot-labs/hdkd-helpers": "^0.0.31",
34
34
  "@polkadot-labs/schnorrkel-wasm": "0.0.9",
35
35
  "@noble/hashes": "2.2.0",
36
36
  "@noble/ciphers": "2.2.0",
37
37
  "@scure/sr25519": "2.2.0",
38
38
  "polkadot-api": ">=2",
39
- "nanoid": "5.1.11",
39
+ "nanoid": "6.0.0",
40
40
  "neverthrow": "^8.2.0",
41
41
  "scale-ts": "1.6.1"
42
42
  },