@novasamatech/statement-store 0.8.0-0 → 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 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
- export declare function deriveSr25519PublicKey(secret: Uint8Array): Uint8Array;
11
- export declare function signWithSr25519Secret(secret: Uint8Array, message: Uint8Array): Uint8Array<ArrayBufferLike> & Uint8Array<ArrayBuffer>;
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, getPublicKey as sr25519GetPublicKey, secretFromSeed as sr25519SecretFromSeed, sign as sr25519Sign, verify as sr25519Verify, } from '@scure/sr25519';
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 sr25519GetPublicKey(secret);
69
+ return substrateSr25519PublicKey(secret);
65
70
  }
66
71
  export function signWithSr25519Secret(secret, message) {
67
- return sr25519Sign(secret, message);
72
+ return substrateSr25519Sign(secret, message);
68
73
  }
69
74
  export function verifySr25519Signature(message, signature, publicKey) {
70
- return sr25519Verify(message, signature, publicKey);
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';
@@ -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 submitStatementData(channel, topicSessionId, data) {
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
  });
@@ -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-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-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.9",
38
+ "nanoid": "5.1.11",
38
39
  "neverthrow": "^8.2.0",
39
40
  "scale-ts": "1.6.1"
40
41
  },