walletpair-sdk 1.0.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.
Files changed (95) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +415 -0
  3. package/dist/ble/framing.d.ts +23 -0
  4. package/dist/ble/framing.d.ts.map +1 -0
  5. package/dist/ble/framing.js +83 -0
  6. package/dist/ble/framing.js.map +1 -0
  7. package/dist/ble/index.d.ts +9 -0
  8. package/dist/ble/index.d.ts.map +1 -0
  9. package/dist/ble/index.js +9 -0
  10. package/dist/ble/index.js.map +1 -0
  11. package/dist/ble/web-ble-transport.d.ts +29 -0
  12. package/dist/ble/web-ble-transport.d.ts.map +1 -0
  13. package/dist/ble/web-ble-transport.js +93 -0
  14. package/dist/ble/web-ble-transport.js.map +1 -0
  15. package/dist/crypto.d.ts +102 -0
  16. package/dist/crypto.d.ts.map +1 -0
  17. package/dist/crypto.js +279 -0
  18. package/dist/crypto.js.map +1 -0
  19. package/dist/dapp-session.d.ts +106 -0
  20. package/dist/dapp-session.d.ts.map +1 -0
  21. package/dist/dapp-session.js +918 -0
  22. package/dist/dapp-session.js.map +1 -0
  23. package/dist/emitter.d.ts +16 -0
  24. package/dist/emitter.d.ts.map +1 -0
  25. package/dist/emitter.js +41 -0
  26. package/dist/emitter.js.map +1 -0
  27. package/dist/evm/eip1193.d.ts +83 -0
  28. package/dist/evm/eip1193.d.ts.map +1 -0
  29. package/dist/evm/eip1193.js +270 -0
  30. package/dist/evm/eip1193.js.map +1 -0
  31. package/dist/evm/index.d.ts +8 -0
  32. package/dist/evm/index.d.ts.map +1 -0
  33. package/dist/evm/index.js +8 -0
  34. package/dist/evm/index.js.map +1 -0
  35. package/dist/evm/wagmi.d.ts +118 -0
  36. package/dist/evm/wagmi.d.ts.map +1 -0
  37. package/dist/evm/wagmi.js +205 -0
  38. package/dist/evm/wagmi.js.map +1 -0
  39. package/dist/index.d.ts +22 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +24 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/types.d.ts +225 -0
  44. package/dist/types.d.ts.map +1 -0
  45. package/dist/types.js +31 -0
  46. package/dist/types.js.map +1 -0
  47. package/dist/wallet-session.d.ts +107 -0
  48. package/dist/wallet-session.d.ts.map +1 -0
  49. package/dist/wallet-session.js +794 -0
  50. package/dist/wallet-session.js.map +1 -0
  51. package/dist/ws-transport.d.ts +29 -0
  52. package/dist/ws-transport.d.ts.map +1 -0
  53. package/dist/ws-transport.js +79 -0
  54. package/dist/ws-transport.js.map +1 -0
  55. package/package.json +55 -0
  56. package/src/__tests__/adversarial/crypto-attacks.test.ts +557 -0
  57. package/src/__tests__/adversarial/malicious-dapp.test.ts +505 -0
  58. package/src/__tests__/adversarial/malicious-relay.test.ts +528 -0
  59. package/src/__tests__/adversarial/malicious-wallet.test.ts +467 -0
  60. package/src/__tests__/spec-compliance/canonical-json.test.ts +227 -0
  61. package/src/__tests__/spec-compliance/crypto-vectors.test.ts +321 -0
  62. package/src/__tests__/spec-compliance/message-format.test.ts +356 -0
  63. package/src/__tests__/spec-compliance/sequence-numbers.test.ts +300 -0
  64. package/src/__tests__/spec-compliance/state-machine.test.ts +364 -0
  65. package/src/ble/framing.test.ts +196 -0
  66. package/src/ble/framing.ts +100 -0
  67. package/src/ble/index.ts +18 -0
  68. package/src/ble/web-ble-transport.test.ts +192 -0
  69. package/src/ble/web-ble-transport.ts +116 -0
  70. package/src/ble/web-bluetooth.d.ts +47 -0
  71. package/src/canonical-json.test.ts +612 -0
  72. package/src/crypto-directional.test.ts +263 -0
  73. package/src/crypto-hardening.test.ts +529 -0
  74. package/src/crypto.test.ts +635 -0
  75. package/src/crypto.ts +405 -0
  76. package/src/dapp-session.test.ts +647 -0
  77. package/src/dapp-session.ts +1004 -0
  78. package/src/emitter.test.ts +169 -0
  79. package/src/emitter.ts +45 -0
  80. package/src/evm/eip1193.test.ts +365 -0
  81. package/src/evm/eip1193.ts +346 -0
  82. package/src/evm/index.ts +19 -0
  83. package/src/evm/wagmi.test.ts +396 -0
  84. package/src/evm/wagmi.ts +321 -0
  85. package/src/index.ts +86 -0
  86. package/src/integration.test.ts +385 -0
  87. package/src/security.test.ts +430 -0
  88. package/src/sequence-validation.test.ts +1185 -0
  89. package/src/test-helpers.ts +216 -0
  90. package/src/types.test.ts +82 -0
  91. package/src/types.ts +305 -0
  92. package/src/wallet-session.test.ts +683 -0
  93. package/src/wallet-session.ts +922 -0
  94. package/src/ws-transport.test.ts +231 -0
  95. package/src/ws-transport.ts +92 -0
@@ -0,0 +1,612 @@
1
+ /**
2
+ * Exhaustive canonical JSON tests.
3
+ *
4
+ * Tests every edge case, boundary condition, and cross-implementation
5
+ * compatibility concern for the canonicalJson function per protocol §6.2.
6
+ *
7
+ * Categories:
8
+ * A. Key sorting (lexicographic UTF-16 / UTF-8)
9
+ * B. Value types (null, boolean, number, string, array, object)
10
+ * C. Number edge cases
11
+ * D. String edge cases (unicode, escaping, surrogates)
12
+ * E. Structural edge cases (nesting, empty, mixed)
13
+ * F. undefined handling
14
+ * G. Determinism and idempotency
15
+ * H. Cross-implementation compatibility vectors
16
+ */
17
+
18
+ import { describe, it, expect } from 'vitest';
19
+ import { canonicalJson, sha256Hex } from './crypto.js';
20
+
21
+ // ═══════════════════════════════════════════════════════════════════════
22
+ // A. Key sorting
23
+ // ═══════════════════════════════════════════════════════════════════════
24
+
25
+ describe('canonicalJson — key sorting', () => {
26
+ it('sorts ASCII keys lexicographically', () => {
27
+ expect(canonicalJson({ z: 1, a: 2, m: 3 })).toBe('{"a":2,"m":3,"z":1}');
28
+ });
29
+
30
+ it('uppercase sorts before lowercase (UTF-16 order)', () => {
31
+ // 'A' = 0x41, 'a' = 0x61 → 'A' < 'a'
32
+ expect(canonicalJson({ a: 1, A: 2 })).toBe('{"A":2,"a":1}');
33
+ });
34
+
35
+ it('digits sort before letters', () => {
36
+ // '0' = 0x30, 'A' = 0x41
37
+ expect(canonicalJson({ a: 1, '0': 2 })).toBe('{"0":2,"a":1}');
38
+ });
39
+
40
+ it('empty string key sorts first', () => {
41
+ expect(canonicalJson({ b: 2, '': 0, a: 1 })).toBe('{"":0,"a":1,"b":2}');
42
+ });
43
+
44
+ it('underscore sorts between uppercase Z and lowercase a', () => {
45
+ // '_' = 0x5F, 'Z' = 0x5A, 'a' = 0x61
46
+ expect(canonicalJson({ a: 1, _: 2, Z: 3 })).toBe('{"Z":3,"_":2,"a":1}');
47
+ });
48
+
49
+ it('numeric string keys sort lexicographically not numerically', () => {
50
+ // "10" < "2" lexicographically because '1' < '2'
51
+ expect(canonicalJson({ '2': 'b', '10': 'a', '1': 'c' })).toBe('{"1":"c","10":"a","2":"b"}');
52
+ });
53
+
54
+ it('sorts keys with common prefixes correctly', () => {
55
+ expect(canonicalJson({ ab: 1, abc: 2, a: 3 })).toBe('{"a":3,"ab":1,"abc":2}');
56
+ });
57
+
58
+ it('handles single-key object', () => {
59
+ expect(canonicalJson({ x: 1 })).toBe('{"x":1}');
60
+ });
61
+
62
+ it('already-sorted keys produce same output', () => {
63
+ expect(canonicalJson({ a: 1, b: 2, c: 3 })).toBe('{"a":1,"b":2,"c":3}');
64
+ });
65
+
66
+ it('reverse-sorted keys are reordered', () => {
67
+ expect(canonicalJson({ c: 3, b: 2, a: 1 })).toBe('{"a":1,"b":2,"c":3}');
68
+ });
69
+
70
+ it('sorts unicode keys by UTF-16 code unit order', () => {
71
+ // For BMP characters, UTF-16 and UTF-8 sort order match for ASCII
72
+ // But for non-ASCII: é (U+00E9) vs ê (U+00EA) → é < ê
73
+ expect(canonicalJson({ 'ê': 2, 'é': 1 })).toBe('{"é":1,"ê":2}');
74
+ });
75
+
76
+ it('handles keys with special JSON characters', () => {
77
+ expect(canonicalJson({ 'a"b': 1, 'a\\c': 2 })).toBe('{"a\\"b":1,"a\\\\c":2}');
78
+ });
79
+ });
80
+
81
+ // ═══════════════════════════════════════════════════════════════════════
82
+ // B. Value types
83
+ // ═══════════════════════════════════════════════════════════════════════
84
+
85
+ describe('canonicalJson — value types', () => {
86
+ it('null → "null"', () => {
87
+ expect(canonicalJson(null)).toBe('null');
88
+ });
89
+
90
+ it('true → "true"', () => {
91
+ expect(canonicalJson(true)).toBe('true');
92
+ });
93
+
94
+ it('false → "false"', () => {
95
+ expect(canonicalJson(false)).toBe('false');
96
+ });
97
+
98
+ it('integer → decimal string', () => {
99
+ expect(canonicalJson(42)).toBe('42');
100
+ expect(canonicalJson(0)).toBe('0');
101
+ expect(canonicalJson(-1)).toBe('-1');
102
+ });
103
+
104
+ it('string → quoted string', () => {
105
+ expect(canonicalJson('hello')).toBe('"hello"');
106
+ });
107
+
108
+ it('empty string → \'""\'', () => {
109
+ expect(canonicalJson('')).toBe('""');
110
+ });
111
+
112
+ it('array → ordered elements', () => {
113
+ expect(canonicalJson([1, 'two', null, true])).toBe('[1,"two",null,true]');
114
+ });
115
+
116
+ it('empty array → "[]"', () => {
117
+ expect(canonicalJson([])).toBe('[]');
118
+ });
119
+
120
+ it('empty object → "{}"', () => {
121
+ expect(canonicalJson({})).toBe('{}');
122
+ });
123
+
124
+ it('nested null in object', () => {
125
+ expect(canonicalJson({ a: null })).toBe('{"a":null}');
126
+ });
127
+
128
+ it('nested null in array', () => {
129
+ expect(canonicalJson([null, null])).toBe('[null,null]');
130
+ });
131
+ });
132
+
133
+ // ═══════════════════════════════════════════════════════════════════════
134
+ // C. Number edge cases
135
+ // ═══════════════════════════════════════════════════════════════════════
136
+
137
+ describe('canonicalJson — numbers', () => {
138
+ it('negative zero → "0"', () => {
139
+ expect(canonicalJson(-0)).toBe('0');
140
+ // Also in object context
141
+ expect(canonicalJson({ v: -0 })).toBe('{"v":0}');
142
+ });
143
+
144
+ it('floats use shortest representation', () => {
145
+ expect(canonicalJson(1.5)).toBe('1.5');
146
+ expect(canonicalJson(0.1)).toBe('0.1');
147
+ expect(canonicalJson(100.0)).toBe('100'); // no trailing .0
148
+ });
149
+
150
+ it('large integers', () => {
151
+ expect(canonicalJson(1000000)).toBe('1000000');
152
+ expect(canonicalJson(Number.MAX_SAFE_INTEGER)).toBe('9007199254740991');
153
+ });
154
+
155
+ it('very small floats', () => {
156
+ // JS serializes 5e-7 as "5e-7" and 0.000001 as "0.000001"
157
+ expect(canonicalJson(0.000001)).toBe('0.000001');
158
+ });
159
+
160
+ it('scientific notation when JS uses it', () => {
161
+ // JS uses scientific notation for very large/small numbers
162
+ const result = canonicalJson(1e20);
163
+ expect(result).toBe('100000000000000000000');
164
+ // 1e21 triggers scientific notation in JS
165
+ expect(canonicalJson(1e21)).toBe('1e+21');
166
+ });
167
+
168
+ it('NaN → throws (RFC 8785 rejects non-finite numbers)', () => {
169
+ expect(() => canonicalJson(NaN)).toThrow();
170
+ });
171
+
172
+ it('Infinity → throws (RFC 8785 rejects non-finite numbers)', () => {
173
+ expect(() => canonicalJson(Infinity)).toThrow();
174
+ expect(() => canonicalJson(-Infinity)).toThrow();
175
+ });
176
+
177
+ it('NaN and Infinity in objects → throws', () => {
178
+ expect(() => canonicalJson({ a: NaN, b: Infinity })).toThrow();
179
+ });
180
+ });
181
+
182
+ // ═══════════════════════════════════════════════════════════════════════
183
+ // D. String edge cases
184
+ // ═══════════════════════════════════════════════════════════════════════
185
+
186
+ describe('canonicalJson — strings', () => {
187
+ // --- Control characters ---
188
+ it('escapes all C0 control characters (U+0000–U+001F)', () => {
189
+ // Tab, newline, carriage return use short form
190
+ expect(canonicalJson('\t')).toBe('"\\t"');
191
+ expect(canonicalJson('\n')).toBe('"\\n"');
192
+ expect(canonicalJson('\r')).toBe('"\\r"');
193
+ expect(canonicalJson('\b')).toBe('"\\b"');
194
+ expect(canonicalJson('\f')).toBe('"\\f"');
195
+ });
196
+
197
+ it('escapes NUL character', () => {
198
+ const result = canonicalJson('\x00');
199
+ expect(result).toBe('"\\u0000"');
200
+ });
201
+
202
+ it('escapes other C0 controls with \\uXXXX', () => {
203
+ // U+0001 through U+001F (excluding those with short forms)
204
+ const result = canonicalJson('\x01');
205
+ expect(result).toBe('"\\u0001"');
206
+ expect(canonicalJson('\x1f')).toBe('"\\u001f"');
207
+ });
208
+
209
+ // --- Mandatory escapes ---
210
+ it('escapes backslash', () => {
211
+ expect(canonicalJson('\\')).toBe('"\\\\"');
212
+ });
213
+
214
+ it('escapes double quote', () => {
215
+ expect(canonicalJson('"')).toBe('"\\""');
216
+ });
217
+
218
+ it('does NOT escape forward slash', () => {
219
+ expect(canonicalJson('/')).toBe('"/"');
220
+ expect(canonicalJson('a/b/c')).toBe('"a/b/c"');
221
+ });
222
+
223
+ // --- Unicode ---
224
+ it('preserves non-ASCII Unicode as literal UTF-8', () => {
225
+ expect(canonicalJson('中文')).toBe('"中文"');
226
+ expect(canonicalJson('éàü')).toBe('"éàü"');
227
+ expect(canonicalJson('日本語')).toBe('"日本語"');
228
+ });
229
+
230
+ it('handles emoji (supplementary plane)', () => {
231
+ expect(canonicalJson('🔑')).toBe('"🔑"');
232
+ expect(canonicalJson('👨‍👩‍👧')).toBe('"👨‍👩‍👧"');
233
+ });
234
+
235
+ it('handles mixed ASCII and non-ASCII', () => {
236
+ expect(canonicalJson('hello 世界 🌍')).toBe('"hello 世界 🌍"');
237
+ });
238
+
239
+ // --- Long strings ---
240
+ it('handles very long strings', () => {
241
+ const long = 'x'.repeat(10000);
242
+ const result = canonicalJson(long);
243
+ expect(result).toBe(`"${long}"`);
244
+ expect(result.length).toBe(10002); // quotes
245
+ });
246
+
247
+ // --- Strings that look like other types ---
248
+ it('does not confuse string "null" with null', () => {
249
+ expect(canonicalJson('null')).toBe('"null"');
250
+ });
251
+
252
+ it('does not confuse string "true" with true', () => {
253
+ expect(canonicalJson('true')).toBe('"true"');
254
+ });
255
+
256
+ it('does not confuse string "123" with number', () => {
257
+ expect(canonicalJson('123')).toBe('"123"');
258
+ });
259
+ });
260
+
261
+ // ═══════════════════════════════════════════════════════════════════════
262
+ // E. Structural edge cases
263
+ // ═══════════════════════════════════════════════════════════════════════
264
+
265
+ describe('canonicalJson — structural', () => {
266
+ it('deeply nested objects (5 levels)', () => {
267
+ const input = { a: { b: { c: { d: { e: 1 } } } } };
268
+ expect(canonicalJson(input)).toBe('{"a":{"b":{"c":{"d":{"e":1}}}}}');
269
+ });
270
+
271
+ it('deeply nested arrays', () => {
272
+ expect(canonicalJson([[[1]]])).toBe('[[[1]]]');
273
+ });
274
+
275
+ it('mixed array/object nesting with key sorting at each level', () => {
276
+ const input = { z: [{ b: 2, a: 1 }], a: { y: [3], x: [1, 2] } };
277
+ expect(canonicalJson(input)).toBe('{"a":{"x":[1,2],"y":[3]},"z":[{"a":1,"b":2}]}');
278
+ });
279
+
280
+ it('array of objects preserves array order but sorts each object', () => {
281
+ const input = [{ z: 1, a: 2 }, { y: 3, b: 4 }];
282
+ expect(canonicalJson(input)).toBe('[{"a":2,"z":1},{"b":4,"y":3}]');
283
+ });
284
+
285
+ it('object with many keys', () => {
286
+ const obj: Record<string, number> = {};
287
+ for (let i = 0; i < 26; i++) {
288
+ obj[String.fromCharCode(122 - i)] = i; // z=0, y=1, ..., a=25
289
+ }
290
+ const result = canonicalJson(obj);
291
+ // Should start with "a" and end with "z"
292
+ expect(result.startsWith('{"a":25')).toBe(true);
293
+ expect(result.endsWith('"z":0}')).toBe(true);
294
+ });
295
+
296
+ it('handles object with boolean, null, number, string, array, object values', () => {
297
+ const input = { str: 'hello', num: 42, bool: true, nil: null, arr: [1], obj: { x: 1 } };
298
+ expect(canonicalJson(input)).toBe(
299
+ '{"arr":[1],"bool":true,"nil":null,"num":42,"obj":{"x":1},"str":"hello"}',
300
+ );
301
+ });
302
+ });
303
+
304
+ // ═══════════════════════════════════════════════════════════════════════
305
+ // F. undefined handling
306
+ // ═══════════════════════════════════════════════════════════════════════
307
+
308
+ describe('canonicalJson — undefined behavior', () => {
309
+ it('top-level undefined → "null"', () => {
310
+ expect(canonicalJson(undefined)).toBe('null');
311
+ });
312
+
313
+ it('undefined object values are omitted (matches JSON.stringify)', () => {
314
+ // Keys with undefined values are omitted, matching JSON.stringify behavior
315
+ // and RFC 8785 / I-JSON which have no concept of undefined
316
+ const result = canonicalJson({ a: 1, b: undefined, c: 3 });
317
+ expect(result).toBe('{"a":1,"c":3}');
318
+ expect(result).not.toContain('"b"');
319
+ });
320
+
321
+ it('undefined in array → "null"', () => {
322
+ // JSON.stringify([undefined]) → "[null]"
323
+ expect(canonicalJson([undefined])).toBe('[null]');
324
+ expect(canonicalJson([1, undefined, 3])).toBe('[1,null,3]');
325
+ });
326
+ });
327
+
328
+ // ═══════════════════════════════════════════════════════════════════════
329
+ // G. No whitespace
330
+ // ═══════════════════════════════════════════════════════════════════════
331
+
332
+ describe('canonicalJson — no whitespace', () => {
333
+ it('never contains spaces, tabs, or newlines in output', () => {
334
+ const complex = {
335
+ methods: ['wallet_signTransaction', 'wallet_signMessage'],
336
+ events: ['accountsChanged', 'chainChanged'],
337
+ chains: ['eip155:1', 'eip155:137'],
338
+ meta: { name: 'My Wallet', description: 'A test wallet' },
339
+ };
340
+ const result = canonicalJson(complex);
341
+ // Content strings may contain spaces, but structural whitespace should not exist
342
+ // Remove string content to check structural whitespace
343
+ const structural = result.replace(/"[^"]*"/g, '""');
344
+ expect(structural).not.toMatch(/[\s]/);
345
+ });
346
+ });
347
+
348
+ // ═══════════════════════════════════════════════════════════════════════
349
+ // H. Determinism and idempotency
350
+ // ═══════════════════════════════════════════════════════════════════════
351
+
352
+ describe('canonicalJson — determinism', () => {
353
+ it('output is idempotent (parsing output and re-serializing yields same result)', () => {
354
+ const input = { z: [3, 1], a: { y: 'hello', x: true } };
355
+ const first = canonicalJson(input);
356
+ const reparsed = JSON.parse(first);
357
+ const second = canonicalJson(reparsed);
358
+ expect(second).toBe(first);
359
+ });
360
+
361
+ it('different insertion order produces same output', () => {
362
+ const a: Record<string, number> = {};
363
+ a.x = 1; a.a = 2; a.m = 3;
364
+
365
+ const b: Record<string, number> = {};
366
+ b.a = 2; b.m = 3; b.x = 1;
367
+
368
+ expect(canonicalJson(a)).toBe(canonicalJson(b));
369
+ });
370
+
371
+ it('100 runs produce identical output', () => {
372
+ const input = { z: 1, a: [{ c: 3, b: 2 }], m: null };
373
+ const expected = canonicalJson(input);
374
+ for (let i = 0; i < 100; i++) {
375
+ expect(canonicalJson(input)).toBe(expected);
376
+ }
377
+ });
378
+ });
379
+
380
+ // ═══════════════════════════════════════════════════════════════════════
381
+ // I. Cross-implementation compatibility vectors
382
+ // ═══════════════════════════════════════════════════════════════════════
383
+
384
+ describe('canonicalJson — protocol spec test vectors (all SHA-256 verified)', () => {
385
+ function hash(s: string): string {
386
+ return sha256Hex(new TextEncoder().encode(s));
387
+ }
388
+
389
+ it('vector 1: capabilities (key sorting, nested)', () => {
390
+ const output = canonicalJson({
391
+ methods: ['wallet_signTransaction', 'wallet_signMessage'],
392
+ events: ['accountsChanged', 'chainChanged'],
393
+ chains: ['eip155:1', 'eip155:137'],
394
+ });
395
+ const expected = '{"chains":["eip155:1","eip155:137"],"events":["accountsChanged","chainChanged"],"methods":["wallet_signTransaction","wallet_signMessage"]}';
396
+ expect(output).toBe(expected);
397
+ expect(hash(output)).toBe('4da366e2aae26b47b3d90fff52410752348733350ce2525dce7d64510f571333');
398
+ });
399
+
400
+ it('vector 2: join plaintext (nested objects + meta, all fields required)', () => {
401
+ const output = canonicalJson({
402
+ capabilities: {
403
+ methods: ['wallet_signTransaction', 'wallet_signMessage'],
404
+ events: ['accountsChanged', 'chainChanged'],
405
+ chains: ['eip155:1', 'eip155:137'],
406
+ },
407
+ meta: {
408
+ name: 'MyWallet',
409
+ description: 'A multi-chain wallet',
410
+ url: 'https://mywallet.app',
411
+ icon: 'https://mywallet.app/icon.png',
412
+ },
413
+ });
414
+ const expected = '{"capabilities":{"chains":["eip155:1","eip155:137"],"events":["accountsChanged","chainChanged"],"methods":["wallet_signTransaction","wallet_signMessage"]},"meta":{"description":"A multi-chain wallet","icon":"https://mywallet.app/icon.png","name":"MyWallet","url":"https://mywallet.app"}}';
415
+ expect(output).toBe(expected);
416
+ expect(hash(output)).toBe('9f4f3b71b0db39ba8b86173b8c78182799d0a745c68b6e89e5d8f0d3def52594');
417
+ });
418
+
419
+ it('vector 3a: null', () => {
420
+ const output = canonicalJson(null);
421
+ expect(output).toBe('null');
422
+ expect(hash(output)).toBe('74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b');
423
+ });
424
+
425
+ it('vector 3b: true', () => {
426
+ const output = canonicalJson(true);
427
+ expect(output).toBe('true');
428
+ expect(hash(output)).toBe('b5bea41b6c623f7c09f1bf24dcae58ebab3c0cdd90ad966bc43a45b44867e12b');
429
+ });
430
+
431
+ it('vector 3c: 42', () => {
432
+ const output = canonicalJson(42);
433
+ expect(output).toBe('42');
434
+ expect(hash(output)).toBe('73475cb40a568e8da8a045ced110137e159f890ac4da883b6b17dc651b3a8049');
435
+ });
436
+
437
+ it('vector 3d: "hello" (string)', () => {
438
+ const output = canonicalJson('hello');
439
+ expect(output).toBe('"hello"');
440
+ expect(hash(output)).toBe('5aa762ae383fbb727af3c7a36d4940a5b8c40a989452d2304fc958ff3f354e7a');
441
+ });
442
+
443
+ it('vector 4a: empty object {}', () => {
444
+ const output = canonicalJson({});
445
+ expect(output).toBe('{}');
446
+ expect(hash(output)).toBe('44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a');
447
+ });
448
+
449
+ it('vector 4b: empty array []', () => {
450
+ const output = canonicalJson([]);
451
+ expect(output).toBe('[]');
452
+ expect(hash(output)).toBe('4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945');
453
+ });
454
+
455
+ it('vector 5: negative zero → 0', () => {
456
+ const output = canonicalJson(-0);
457
+ expect(output).toBe('0');
458
+ expect(hash(output)).toBe('5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9');
459
+ });
460
+
461
+ it('vector 6: escaped control character (lowercase hex)', () => {
462
+ const output = canonicalJson('\u0001');
463
+ expect(output).toBe('"\\u0001"');
464
+ expect(hash(output)).toBe('b81cfb0a6715e53b373345b49e8ad94eb55fd777519dc539373d0634973c186e');
465
+ });
466
+ });
467
+
468
+ describe('canonicalJson — additional cross-implementation vectors', () => {
469
+ it('empty capabilities with null meta', () => {
470
+ const output = canonicalJson({ capabilities: { methods: [], events: [], chains: [] }, meta: null });
471
+ expect(output).toBe('{"capabilities":{"chains":[],"events":[],"methods":[]},"meta":null}');
472
+ });
473
+
474
+ it('unicode key and value', () => {
475
+ const output = canonicalJson({ name: '钱包', chains: ['eip155:1'] });
476
+ expect(output).toBe('{"chains":["eip155:1"],"name":"钱包"}');
477
+ });
478
+
479
+ it('boolean and null mix with sorting', () => {
480
+ const output = canonicalJson({ z: null, a: true, m: false });
481
+ expect(output).toBe('{"a":true,"m":false,"z":null}');
482
+ });
483
+ });
484
+
485
+ // ═══════════════════════════════════════════════════════════════════════
486
+ // J. RFC 8785 compliance — specific requirements
487
+ // ═══════════════════════════════════════════════════════════════════════
488
+
489
+ describe('canonicalJson — RFC 8785 specific compliance', () => {
490
+ // --- \uXXXX must be lowercase hex (RFC 8785 §3.2.2.2) ---
491
+ it('\\u escapes use lowercase hex digits', () => {
492
+ // U+0001 → \u0001 (not \u0001 with uppercase)
493
+ const result = canonicalJson('\u0001');
494
+ expect(result).toBe('"\\u0001"');
495
+ expect(result).not.toMatch(/\\u[0-9a-f]*[A-F]/); // no uppercase hex digits
496
+ });
497
+
498
+ it('\\u001f uses lowercase f', () => {
499
+ const result = canonicalJson('\u001f');
500
+ expect(result).toBe('"\\u001f"');
501
+ // Verify the 'f' is lowercase (0x66) not uppercase 'F' (0x46)
502
+ expect(result.charAt(6)).toBe('f');
503
+ });
504
+
505
+ it('all C0 control characters (U+0000–U+001F) are properly escaped', () => {
506
+ const shortForms: Record<number, string> = {
507
+ 0x08: '\\b', 0x09: '\\t', 0x0a: '\\n', 0x0c: '\\f', 0x0d: '\\r',
508
+ };
509
+ for (let cp = 0; cp <= 0x1f; cp++) {
510
+ const result = canonicalJson(String.fromCharCode(cp));
511
+ if (shortForms[cp]) {
512
+ expect(result).toBe(`"${shortForms[cp]}"`);
513
+ } else {
514
+ const hex = cp.toString(16).padStart(4, '0');
515
+ expect(result).toBe(`"\\u${hex}"`);
516
+ }
517
+ }
518
+ });
519
+
520
+ // --- Lone surrogates (ES2019 well-formed JSON.stringify) ---
521
+ it('lone high surrogate is escaped as \\udXXX', () => {
522
+ const result = canonicalJson('\uD800');
523
+ expect(result).toBe('"\\ud800"');
524
+ });
525
+
526
+ it('lone low surrogate is escaped as \\udcXX', () => {
527
+ const result = canonicalJson('\uDC00');
528
+ expect(result).toBe('"\\udc00"');
529
+ });
530
+
531
+ it('valid surrogate pair (emoji) is NOT escaped', () => {
532
+ // U+1F511 = 🔑 = \uD83D\uDD11 (surrogate pair)
533
+ const result = canonicalJson('🔑');
534
+ expect(result).toBe('"🔑"');
535
+ expect(result).not.toContain('\\u');
536
+ });
537
+
538
+ // --- Number serialization matches ECMAScript Number.toString() ---
539
+ it('1e20 outputs as full decimal (no scientific notation)', () => {
540
+ expect(canonicalJson(1e20)).toBe('100000000000000000000');
541
+ });
542
+
543
+ it('1e21 uses scientific notation (JS threshold)', () => {
544
+ expect(canonicalJson(1e21)).toBe('1e+21');
545
+ });
546
+
547
+ it('5e-7 uses scientific notation', () => {
548
+ expect(canonicalJson(5e-7)).toBe('5e-7');
549
+ });
550
+
551
+ it('0.000001 uses decimal notation', () => {
552
+ expect(canonicalJson(0.000001)).toBe('0.000001');
553
+ });
554
+
555
+ it('Number.MIN_VALUE in scientific notation', () => {
556
+ expect(canonicalJson(Number.MIN_VALUE)).toBe('5e-324');
557
+ });
558
+
559
+ it('Number.MAX_SAFE_INTEGER preserves precision', () => {
560
+ expect(canonicalJson(9007199254740991)).toBe('9007199254740991');
561
+ });
562
+
563
+ // --- Duplicate key handling ---
564
+ it('JSON.parse deduplicates keys (last value wins)', () => {
565
+ // This tests that our implementation handles the JS-level dedup correctly.
566
+ // True duplicate rejection requires a custom JSON parser, which is out of scope
567
+ // for the SDK (implementers using other languages must reject at parse time).
568
+ const input = JSON.parse('{"a":1,"b":2,"a":3}');
569
+ expect(canonicalJson(input)).toBe('{"a":3,"b":2}');
570
+ });
571
+
572
+ // --- Output encoding ---
573
+ it('output bytes are valid UTF-8', () => {
574
+ const output = canonicalJson({ name: '钱包', emoji: '🔑' });
575
+ const bytes = new TextEncoder().encode(output);
576
+ // TextEncoder always produces UTF-8
577
+ // Verify round-trip
578
+ const decoded = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
579
+ expect(decoded).toBe(output);
580
+ });
581
+
582
+ it('output has no BOM', () => {
583
+ const output = canonicalJson({ test: true });
584
+ const bytes = new TextEncoder().encode(output);
585
+ // UTF-8 BOM = EF BB BF
586
+ expect(bytes[0]).not.toBe(0xef);
587
+ });
588
+ });
589
+
590
+ // ═══════════════════════════════════════════════════════════════════════
591
+ // K. Protocol-specific: sealed payload uses canonical JSON
592
+ // ═══════════════════════════════════════════════════════════════════════
593
+
594
+ describe('canonicalJson — seal/unseal round-trip proves canonical encoding', () => {
595
+ it('object with unsorted keys round-trips correctly through seal/unseal', async () => {
596
+ // This proves that sealPayload uses canonicalJson internally,
597
+ // and unsealPayload returns the canonical form
598
+ const { sealPayload, unsealPayload } = await import('./crypto.js');
599
+ const key = new Uint8Array(32).fill(0xdd);
600
+ const ch = 'ee'.repeat(32);
601
+
602
+ // Input has keys in reverse order
603
+ const input = { z: 1, a: 2, m: 3 };
604
+ const sealed = sealPayload(key, ch, 0, input);
605
+ const { data, plaintextJson } = unsealPayload(key, ch, sealed);
606
+
607
+ // Data round-trips correctly
608
+ expect(data).toEqual(input);
609
+ // The plaintext inside the ciphertext is canonical JSON
610
+ expect(plaintextJson).toBe('{"a":2,"m":3,"z":1}');
611
+ });
612
+ });