walletpair-sdk 1.0.3 → 1.0.5

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