@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 +1 -0
- package/dist/index.js +2 -0
- package/dist/tx/TransactionService.js +2 -0
- package/dist/tx/types.d.ts +3 -0
- package/dist/verify.js +8 -7
- package/dist/wots.d.ts +1 -1
- package/dist/wots.js +25 -4
- package/package.json +1 -1
- package/src/__tests__/roottree-prepare.test.ts +101 -0
- package/src/index.ts +14 -0
- package/src/tx/TransactionService.ts +2 -0
- package/src/tx/types.ts +4 -1
- package/src/verify.ts +7 -6
- package/src/wots.js +10 -4
- package/src/wots.ts +19 -4
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',
|
package/dist/tx/types.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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) =>
|
|
50
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
@@ -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;
|
|
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
|
-
|
|
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) =>
|
|
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) =>
|
|
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
|
-
|
|
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
|
-
|
|
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) =>
|
|
57
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|