@totemsdk/core 1.0.4 → 1.0.6

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/index.d.ts CHANGED
@@ -28,3 +28,4 @@ export declare const MINIMA_CONSTANTS: {
28
28
  readonly ADDRESS_PREFIX: "Mx";
29
29
  readonly NETWORK_ID: 1;
30
30
  };
31
+ export { writeMiniNumber, writeMiniData, writeMiniByte, writeMMREntryNumber, concat, hexToBytes, bytesToHex, bigIntToByteArray, writeMiniString, type Bytes, } from './Streamable.js';
package/dist/index.js CHANGED
@@ -51,3 +51,5 @@ export const MINIMA_CONSTANTS = {
51
51
  ADDRESS_PREFIX: 'Mx',
52
52
  NETWORK_ID: 1
53
53
  };
54
+ // Streamable primitives (low-level serialization building blocks)
55
+ export { writeMiniNumber, writeMiniData, writeMiniByte, writeMMREntryNumber, concat, hexToBytes, bytesToHex, bigIntToByteArray, writeMiniString, } from './Streamable.js';
@@ -38,6 +38,8 @@ export class TransactionService {
38
38
  burn: params.burn || null,
39
39
  paramSet: this.config.paramSet || 'v2-spec',
40
40
  addressIndex: params.addressIndex,
41
+ walletMode: params.walletMode || 'AnonTree',
42
+ perAddressPublicKey: params.perAddressPublicKey ?? null,
41
43
  }, {
42
44
  headers: {
43
45
  'Content-Type': 'application/json',
@@ -14,6 +14,8 @@ export interface PrepareRequest {
14
14
  burn?: string;
15
15
  txId?: string;
16
16
  addressIndex?: number;
17
+ walletMode?: 'AnonTree' | 'RootTree';
18
+ perAddressPublicKey?: string;
17
19
  }
18
20
  export interface PrepareResponse {
19
21
  addressIndex: number;
@@ -28,6 +30,7 @@ export interface PrepareResponse {
28
30
  paramSet: string;
29
31
  leaseId: string;
30
32
  leaseTTL: number;
33
+ perAddressScript?: string | null;
31
34
  }
32
35
  export interface SignRequest {
33
36
  addressIndex: number;
package/dist/verify.js CHANGED
@@ -118,16 +118,17 @@ function timingSafeEqual(a, b) {
118
118
  function cryptoRandomBytes(length) {
119
119
  const buf = new Uint8Array(length);
120
120
  if (typeof globalThis.crypto !== 'undefined' && globalThis.crypto.getRandomValues) {
121
+ // Web Crypto API — available in browsers, Bare/Pear, and Node.js ≥ 19
121
122
  globalThis.crypto.getRandomValues(buf);
122
123
  }
124
+ else if (typeof process !== 'undefined' && process.versions?.node) {
125
+ // Node.js < 19 fallback — dynamic import avoids static resolution in Bare/bundlers
126
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
127
+ const nodeCrypto = Function('return require')()('crypto');
128
+ nodeCrypto.randomFillSync(buf);
129
+ }
123
130
  else {
124
- try {
125
- const { randomFillSync } = require('crypto');
126
- randomFillSync(buf);
127
- }
128
- catch {
129
- throw new Error('No cryptographically secure RNG available');
130
- }
131
+ throw new Error('No cryptographically secure RNG available in this runtime');
131
132
  }
132
133
  return buf;
133
134
  }
package/dist/wots.d.ts CHANGED
@@ -33,7 +33,7 @@ export declare function disableWotsLogger(): void;
33
33
  export declare function isWotsDebugEnabled(): boolean;
34
34
  export declare const F: (x: Uint8Array) => Uint8Array<ArrayBufferLike>;
35
35
  export declare const hex: (u: Uint8Array) => string;
36
- export declare const fromHex: (h: string) => Uint8Array<ArrayBuffer>;
36
+ export declare const fromHex: (h: string) => Uint8Array;
37
37
  export declare const concatBytes: (...arrs: Uint8Array[]) => Uint8Array<ArrayBuffer>;
38
38
  export declare const u16be: (n: number) => Uint8Array<ArrayBuffer>;
39
39
  export declare const u32be: (n: number) => Uint8Array<ArrayBuffer>;
package/dist/wots.js CHANGED
@@ -46,8 +46,19 @@ export function isWotsDebugEnabled() {
46
46
  }
47
47
  // === Canonical helpers ===
48
48
  export const F = (x) => sha3_256(x);
49
- export const hex = (u) => Buffer.from(u).toString('hex');
50
- export const fromHex = (h) => Uint8Array.from(Buffer.from(h.replace(/^0x/, ''), 'hex'));
49
+ export const hex = (u) => {
50
+ let s = '';
51
+ for (let i = 0; i < u.length; i++)
52
+ s += u[i].toString(16).padStart(2, '0');
53
+ return s;
54
+ };
55
+ export const fromHex = (h) => {
56
+ const s = h.replace(/^0x/, '');
57
+ const out = new Uint8Array(s.length >> 1);
58
+ for (let i = 0; i < out.length; i++)
59
+ out[i] = parseInt(s.slice(i * 2, i * 2 + 2), 16);
60
+ return out;
61
+ };
51
62
  export const concatBytes = (...arrs) => {
52
63
  const len = arrs.reduce((n, a) => n + a.length, 0);
53
64
  const out = new Uint8Array(len);
@@ -490,7 +501,12 @@ export function wotsVerify(sig, message, pkFull, ps = getParamSet()) {
490
501
  }
491
502
  // Compare FULL 1088-byte reconstructed key to expected FULL public key
492
503
  // This matches Java: resp.isEqual(zPublicKey) where both are 1088 bytes
493
- return Buffer.from(buf).equals(Buffer.from(pkFull));
504
+ if (buf.length !== pkFull.length)
505
+ return false;
506
+ let diff = 0;
507
+ for (let i = 0; i < buf.length; i++)
508
+ diff |= buf[i] ^ pkFull[i];
509
+ return diff === 0;
494
510
  }
495
511
  /**
496
512
  * Legacy verify function that accepts a 32-byte digest
@@ -512,7 +528,12 @@ export function wotsVerifyDigest(sig, message, pkDigest, ps = getParamSet()) {
512
528
  buf.set(top, j * 32);
513
529
  }
514
530
  const recomputed = F(buf);
515
- return Buffer.from(recomputed).equals(Buffer.from(pkDigest));
531
+ if (recomputed.length !== pkDigest.length)
532
+ return false;
533
+ let diff = 0;
534
+ for (let i = 0; i < recomputed.length; i++)
535
+ diff |= recomputed[i] ^ pkDigest[i];
536
+ return diff === 0;
516
537
  }
517
538
  /**
518
539
  * Generate WOTS public key from seed (convenience wrapper)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@totemsdk/core",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "Core cryptographic primitives for Totem SDK",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -0,0 +1,101 @@
1
+ /**
2
+ * RootTree prepare() propagation tests
3
+ *
4
+ * Verifies that TransactionService.prepare() correctly forwards walletMode
5
+ * and perAddressPublicKey in the POST body so the /prepare endpoint can
6
+ * apply RootTree-specific validation and return perAddressScript.
7
+ */
8
+
9
+ import { TransactionService } from '../tx/TransactionService.js';
10
+ import type { TransactionServiceConfig } from '../tx/TransactionService.js';
11
+ import type { HttpClient } from '../adapters/index.js';
12
+
13
+ function makeMockHttp(capturedBodies: any[]): HttpClient {
14
+ return {
15
+ post: async <T>(_url: string, body: any): Promise<{ ok: boolean; status: number; data: T }> => {
16
+ capturedBodies.push({ body });
17
+ return {
18
+ ok: true,
19
+ status: 200,
20
+ data: {
21
+ addressIndex: 0,
22
+ l1: 0,
23
+ l2: 0,
24
+ leaseToken: 'mock-lease-token',
25
+ digestTx: '0x' + 'ab'.repeat(32),
26
+ digestL2: null,
27
+ digestL3: null,
28
+ txId: 'tx-mock',
29
+ rootPublicKey: '0x' + 'ff'.repeat(32),
30
+ paramSet: 'v2-spec',
31
+ leaseId: 'lease-mock',
32
+ leaseTTL: 120000,
33
+ perAddressScript: 'RETURN SIGNEDBY(0x' + 'ff'.repeat(32) + ')',
34
+ } as T,
35
+ };
36
+ },
37
+ get: async <T>(): Promise<{ ok: boolean; status: number; data: T }> => ({
38
+ ok: true, status: 200, data: {} as T,
39
+ }),
40
+ };
41
+ }
42
+
43
+ const config: TransactionServiceConfig = {
44
+ baseUrl: 'https://api.example.com',
45
+ apiKey: 'test-api-key',
46
+ paramSet: 'v2-spec',
47
+ };
48
+
49
+ const MOCK_ROOT_PK = '0x' + 'aa'.repeat(32);
50
+ const MOCK_PER_ADDR_PK = '0x' + 'bb'.repeat(32);
51
+
52
+ describe('TransactionService.prepare() — RootTree propagation', () => {
53
+ it('sends walletMode=AnonTree by default', async () => {
54
+ const bodies: any[] = [];
55
+ const svc = new TransactionService(makeMockHttp(bodies), config);
56
+
57
+ await svc.prepare(
58
+ { to: 'MxABC', amount: '1', addressIndex: 0 },
59
+ MOCK_ROOT_PK
60
+ );
61
+
62
+ expect(bodies).toHaveLength(1);
63
+ expect(bodies[0].body.walletMode).toBe('AnonTree');
64
+ expect(bodies[0].body.perAddressPublicKey).toBeNull();
65
+ });
66
+
67
+ it('forwards walletMode=RootTree and perAddressPublicKey in POST body', async () => {
68
+ const bodies: any[] = [];
69
+ const svc = new TransactionService(makeMockHttp(bodies), config);
70
+
71
+ await svc.prepare(
72
+ {
73
+ to: 'MxABC',
74
+ amount: '1',
75
+ addressIndex: 0,
76
+ walletMode: 'RootTree',
77
+ perAddressPublicKey: MOCK_PER_ADDR_PK,
78
+ },
79
+ MOCK_ROOT_PK
80
+ );
81
+
82
+ expect(bodies).toHaveLength(1);
83
+ const sent = bodies[0].body;
84
+ expect(sent.walletMode).toBe('RootTree');
85
+ expect(sent.perAddressPublicKey).toBe(MOCK_PER_ADDR_PK);
86
+ expect(sent.rootPublicKey).toBe(MOCK_ROOT_PK);
87
+ });
88
+
89
+ it('PrepareResponse includes perAddressScript field from server', async () => {
90
+ const bodies: any[] = [];
91
+ const svc = new TransactionService(makeMockHttp(bodies), config);
92
+
93
+ const result = await svc.prepare(
94
+ { to: 'MxABC', amount: '1', addressIndex: 0, walletMode: 'RootTree', perAddressPublicKey: MOCK_PER_ADDR_PK },
95
+ MOCK_ROOT_PK
96
+ );
97
+
98
+ expect(result.perAddressScript).toBeDefined();
99
+ expect(typeof result.perAddressScript).toBe('string');
100
+ });
101
+ });
package/src/index.ts CHANGED
@@ -204,3 +204,17 @@ export const MINIMA_CONSTANTS = {
204
204
  ADDRESS_PREFIX: 'Mx',
205
205
  NETWORK_ID: 1
206
206
  } as const;
207
+
208
+ // Streamable primitives (low-level serialization building blocks)
209
+ export {
210
+ writeMiniNumber,
211
+ writeMiniData,
212
+ writeMiniByte,
213
+ writeMMREntryNumber,
214
+ concat,
215
+ hexToBytes,
216
+ bytesToHex,
217
+ bigIntToByteArray,
218
+ writeMiniString,
219
+ type Bytes,
220
+ } from './Streamable.js';
@@ -83,6 +83,8 @@ export class TransactionService {
83
83
  burn: params.burn || null,
84
84
  paramSet: this.config.paramSet || 'v2-spec',
85
85
  addressIndex: params.addressIndex,
86
+ walletMode: params.walletMode || 'AnonTree',
87
+ perAddressPublicKey: params.perAddressPublicKey ?? null,
86
88
  }, {
87
89
  headers: {
88
90
  'Content-Type': 'application/json',
package/src/tx/types.ts CHANGED
@@ -15,7 +15,9 @@ export interface PrepareRequest {
15
15
  tokenId?: string;
16
16
  burn?: string;
17
17
  txId?: string;
18
- addressIndex?: number; // Required by v2-spec backend (0-63, must match the coin's address)
18
+ addressIndex?: number; // Required by v2-spec backend (0-63, must match the coin's address)
19
+ walletMode?: 'AnonTree' | 'RootTree'; // Key derivation scheme — omit for legacy/AnonTree
20
+ perAddressPublicKey?: string; // Hex pubkey of per-address TreeKey root (required for RootTree validation)
19
21
  }
20
22
 
21
23
  export interface PrepareResponse {
@@ -31,6 +33,7 @@ export interface PrepareResponse {
31
33
  paramSet: string;
32
34
  leaseId: string;
33
35
  leaseTTL: number;
36
+ perAddressScript?: string | null; // RETURN SIGNEDBY(<perAddressPublicKey>) — present in RootTree mode
34
37
  }
35
38
 
36
39
  export interface SignRequest {
package/src/verify.ts CHANGED
@@ -157,14 +157,15 @@ function timingSafeEqual(a: Uint8Array, b: Uint8Array): boolean {
157
157
  function cryptoRandomBytes(length: number): Uint8Array {
158
158
  const buf = new Uint8Array(length);
159
159
  if (typeof globalThis.crypto !== 'undefined' && globalThis.crypto.getRandomValues) {
160
+ // Web Crypto API — available in browsers, Bare/Pear, and Node.js ≥ 19
160
161
  globalThis.crypto.getRandomValues(buf);
162
+ } else if (typeof process !== 'undefined' && process.versions?.node) {
163
+ // Node.js < 19 fallback — dynamic import avoids static resolution in Bare/bundlers
164
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
165
+ const nodeCrypto = Function('return require')()('crypto') as { randomFillSync: (buf: Uint8Array) => void };
166
+ nodeCrypto.randomFillSync(buf);
161
167
  } else {
162
- try {
163
- const { randomFillSync } = require('crypto');
164
- randomFillSync(buf);
165
- } catch {
166
- throw new Error('No cryptographically secure RNG available');
167
- }
168
+ throw new Error('No cryptographically secure RNG available in this runtime');
168
169
  }
169
170
  return buf;
170
171
  }
package/src/wots.js CHANGED
@@ -70,9 +70,9 @@ function isWotsDebugEnabled() {
70
70
  // === Canonical helpers ===
71
71
  const F = (x) => (0, sha3_1.sha3_256)(x);
72
72
  exports.F = F;
73
- const hex = (u) => Buffer.from(u).toString('hex');
73
+ const hex = (u) => { let s = ''; for (let i = 0; i < u.length; i++) s += u[i].toString(16).padStart(2, '0'); return s; };
74
74
  exports.hex = hex;
75
- const fromHex = (h) => Uint8Array.from(Buffer.from(h.replace(/^0x/, ''), 'hex'));
75
+ const fromHex = (h) => { const s = h.replace(/^0x/, ''); const out = new Uint8Array(s.length >> 1); for (let i = 0; i < out.length; i++) out[i] = parseInt(s.slice(i * 2, i * 2 + 2), 16); return out; };
76
76
  exports.fromHex = fromHex;
77
77
  const concatBytes = (...arrs) => {
78
78
  const len = arrs.reduce((n, a) => n + a.length, 0);
@@ -511,7 +511,10 @@ function wotsVerify(sig, message, pkFull, ps = (0, params_1.getParamSet)()) {
511
511
  }
512
512
  // Compare FULL 1088-byte reconstructed key to expected FULL public key
513
513
  // This matches Java: resp.isEqual(zPublicKey) where both are 1088 bytes
514
- return Buffer.from(buf).equals(Buffer.from(pkFull));
514
+ if (buf.length !== pkFull.length) return false;
515
+ let diff = 0;
516
+ for (let i = 0; i < buf.length; i++) diff |= buf[i] ^ pkFull[i];
517
+ return diff === 0;
515
518
  }
516
519
  /**
517
520
  * Legacy verify function that accepts a 32-byte digest
@@ -533,7 +536,10 @@ function wotsVerifyDigest(sig, message, pkDigest, ps = (0, params_1.getParamSet)
533
536
  buf.set(top, j * 32);
534
537
  }
535
538
  const recomputed = (0, exports.F)(buf);
536
- return Buffer.from(recomputed).equals(Buffer.from(pkDigest));
539
+ if (recomputed.length !== pkDigest.length) return false;
540
+ let diff = 0;
541
+ for (let i = 0; i < recomputed.length; i++) diff |= recomputed[i] ^ pkDigest[i];
542
+ return diff === 0;
537
543
  }
538
544
  /**
539
545
  * Generate WOTS public key from seed (convenience wrapper)
package/src/wots.ts CHANGED
@@ -53,8 +53,17 @@ export function isWotsDebugEnabled(): boolean {
53
53
 
54
54
  // === Canonical helpers ===
55
55
  export const F = (x: Uint8Array) => sha3_256(x);
56
- export const hex = (u: Uint8Array) => Buffer.from(u).toString('hex');
57
- export const fromHex = (h: string) => Uint8Array.from(Buffer.from(h.replace(/^0x/, ''), 'hex'));
56
+ export const hex = (u: Uint8Array): string => {
57
+ let s = '';
58
+ for (let i = 0; i < u.length; i++) s += u[i].toString(16).padStart(2, '0');
59
+ return s;
60
+ };
61
+ export const fromHex = (h: string): Uint8Array => {
62
+ const s = h.replace(/^0x/, '');
63
+ const out = new Uint8Array(s.length >> 1);
64
+ for (let i = 0; i < out.length; i++) out[i] = parseInt(s.slice(i * 2, i * 2 + 2), 16);
65
+ return out;
66
+ };
58
67
  export const concatBytes = (...arrs: Uint8Array[]) => {
59
68
  const len = arrs.reduce((n, a) => n + a.length, 0);
60
69
  const out = new Uint8Array(len);
@@ -579,7 +588,10 @@ export function wotsVerify(sig: Uint8Array, message: Uint8Array, pkFull: Uint8Ar
579
588
 
580
589
  // Compare FULL 1088-byte reconstructed key to expected FULL public key
581
590
  // This matches Java: resp.isEqual(zPublicKey) where both are 1088 bytes
582
- return Buffer.from(buf).equals(Buffer.from(pkFull));
591
+ if (buf.length !== pkFull.length) return false;
592
+ let diff = 0;
593
+ for (let i = 0; i < buf.length; i++) diff |= buf[i] ^ pkFull[i];
594
+ return diff === 0;
583
595
  }
584
596
 
585
597
  /**
@@ -606,7 +618,10 @@ export function wotsVerifyDigest(sig: Uint8Array, message: Uint8Array, pkDigest:
606
618
  }
607
619
 
608
620
  const recomputed = F(buf);
609
- return Buffer.from(recomputed).equals(Buffer.from(pkDigest));
621
+ if (recomputed.length !== pkDigest.length) return false;
622
+ let diff = 0;
623
+ for (let i = 0; i < recomputed.length; i++) diff |= recomputed[i] ^ pkDigest[i];
624
+ return diff === 0;
610
625
  }
611
626
 
612
627