walletpair-sdk 1.0.2 → 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 +3 -2
  34. package/dist/ws-transport.d.ts.map +1 -1
  35. package/dist/ws-transport.js +13 -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 +52 -41
@@ -1,39 +1,36 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import { WalletSession } from './wallet-session.js';
3
- import { MockTransport } from './test-helpers.js';
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
2
+ import type { SessionCryptoContext } from './crypto.js'
4
3
  import {
5
- generateX25519KeyPair,
6
- generateChannelId,
4
+ b64urlDecode,
7
5
  buildPairingUri,
6
+ computeSessionFingerprint,
8
7
  computeSharedSecret,
9
- deriveSessionKey,
10
8
  deriveDirectionalSessionKeys,
11
9
  deriveJoinEncryptionKey,
12
- computeSessionFingerprint,
10
+ deriveSessionKey,
11
+ generateChannelId,
12
+ generateX25519KeyPair,
13
13
  sealPayload,
14
- unsealPayload,
15
14
  unsealJoin,
16
- b64urlEncode,
17
- b64urlDecode,
18
- bytesToHex,
19
- hexToBytes,
20
- } from './crypto.js';
21
- import type { AadHeader, SessionCryptoContext } from './crypto.js';
22
- import type { ProtocolMessage } from './types.js';
23
-
24
- function flushMicrotasks(): Promise<void> {
25
- return new Promise((r) => setTimeout(r, 10));
15
+ unsealPayload,
16
+ } from './crypto.js'
17
+ import { MockTransport } from './test-helpers.js'
18
+ import type { ProtocolMessage } from './types.js'
19
+ import { WalletSession } from './wallet-session.js'
20
+
21
+ function _flushMicrotasks(): Promise<void> {
22
+ return new Promise((r) => setTimeout(r, 10))
26
23
  }
27
24
 
28
25
  describe('WalletSession', () => {
29
- let transport: MockTransport;
30
- let session: WalletSession;
31
- let dappKp: ReturnType<typeof generateX25519KeyPair>;
32
- let channelId: string;
33
- let relayUrl: string;
26
+ let transport: MockTransport
27
+ let session: WalletSession
28
+ let dappKp: ReturnType<typeof generateX25519KeyPair>
29
+ let channelId: string
30
+ let relayUrl: string
34
31
 
35
32
  beforeEach(() => {
36
- transport = new MockTransport();
33
+ transport = new MockTransport()
37
34
  session = new WalletSession({
38
35
  transport,
39
36
  capabilities: {
@@ -41,12 +38,18 @@ describe('WalletSession', () => {
41
38
  events: ['accountsChanged', 'chainChanged'],
42
39
  chains: ['eip155:1'],
43
40
  },
44
- meta: { name: 'Test Wallet', description: 'Test', url: 'https://test.com', icon: 'https://test.com/icon.png', address: '0xtest' },
45
- });
46
- dappKp = generateX25519KeyPair();
47
- channelId = generateChannelId();
48
- relayUrl = 'ws://localhost:8080/v1';
49
- });
41
+ meta: {
42
+ name: 'Test Wallet',
43
+ description: 'Test',
44
+ url: 'https://test.com',
45
+ icon: 'https://test.com/icon.png',
46
+ address: '0xtest',
47
+ },
48
+ })
49
+ dappKp = generateX25519KeyPair()
50
+ channelId = generateChannelId()
51
+ relayUrl = 'ws://localhost:8080/v1'
52
+ })
50
53
 
51
54
  function makePairingUri(): string {
52
55
  return buildPairingUri({
@@ -56,99 +59,107 @@ describe('WalletSession', () => {
56
59
  name: 'Test dApp',
57
60
  url: 'https://test.com',
58
61
  icon: 'https://test.com/icon.png',
59
- });
62
+ })
60
63
  }
61
64
 
62
65
  function receiveConnected(): void {
63
66
  transport.receive({
64
- v: 1, t: 'ready', ch: channelId,
65
- ts: Date.now(), from: '_adapter',
67
+ v: 1,
68
+ t: 'ready',
69
+ ch: channelId,
70
+ ts: Date.now(),
71
+ from: '_adapter',
66
72
  body: { state: 'connected', reconnect: false, remote: dappKp.publicKeyB64 },
67
- } as ProtocolMessage);
73
+ } as ProtocolMessage)
68
74
  }
69
75
 
70
76
  describe('joinFromUri', () => {
71
77
  it('starts in idle phase', () => {
72
- expect(session.phase).toBe('idle');
73
- });
78
+ expect(session.phase).toBe('idle')
79
+ })
74
80
 
75
81
  it('parses URI and sends join message with sealed_join', async () => {
76
- const uri = makePairingUri();
77
- await session.joinFromUri(uri);
82
+ const uri = makePairingUri()
83
+ await session.joinFromUri(uri)
78
84
 
79
- expect(session.channelId).toBe(channelId);
80
- expect(session.phase).toBe('waiting_accept');
85
+ expect(session.channelId).toBe(channelId)
86
+ expect(session.phase).toBe('waiting_accept')
81
87
 
82
- const joinMsg = transport.sent.find(m => m.t === 'join');
83
- expect(joinMsg).toBeTruthy();
88
+ const joinMsg = transport.sent.find((m) => m.t === 'join')
89
+ expect(joinMsg).toBeTruthy()
84
90
  // Capabilities are now inside sealed_join, not plaintext
85
- expect((joinMsg as any).body.sealed_join).toBeTruthy();
86
- });
91
+ expect((joinMsg as any).body.sealed_join).toBeTruthy()
92
+ })
87
93
 
88
94
  it('computes and emits session fingerprint', async () => {
89
- const handler = vi.fn();
90
- session.on('sessionFingerprint', handler);
95
+ const handler = vi.fn()
96
+ session.on('sessionFingerprint', handler)
91
97
 
92
- const uri = makePairingUri();
93
- await session.joinFromUri(uri);
98
+ const uri = makePairingUri()
99
+ await session.joinFromUri(uri)
94
100
 
95
- expect(handler).toHaveBeenCalled();
96
- expect(session.sessionFingerprint).toMatch(/^\d{4}$/);
97
- });
101
+ expect(handler).toHaveBeenCalled()
102
+ expect(session.sessionFingerprint).toMatch(/^\d{4}$/)
103
+ })
98
104
 
99
105
  it('session fingerprint matches dApp side derivation', async () => {
100
- const uri = makePairingUri();
101
- await session.joinFromUri(uri);
106
+ const uri = makePairingUri()
107
+ await session.joinFromUri(uri)
102
108
 
103
109
  // Both sides use computeSessionFingerprint(channelId, dappPubKeyB64)
104
- const dappFingerprint = computeSessionFingerprint(channelId, dappKp.publicKeyB64);
110
+ const dappFingerprint = computeSessionFingerprint(channelId, dappKp.publicKeyB64)
105
111
 
106
- expect(session.sessionFingerprint).toBe(dappFingerprint);
107
- });
112
+ expect(session.sessionFingerprint).toBe(dappFingerprint)
113
+ })
108
114
 
109
115
  it('transitions to connected on ready.connected', async () => {
110
- const phases: string[] = [];
111
- session.on('phase', (p) => phases.push(p));
116
+ const phases: string[] = []
117
+ session.on('phase', (p) => phases.push(p))
112
118
 
113
- await session.joinFromUri(makePairingUri());
119
+ await session.joinFromUri(makePairingUri())
114
120
 
115
- receiveConnected();
121
+ receiveConnected()
116
122
 
117
- expect(session.phase).toBe('connected');
118
- expect(phases).toContain('waiting_accept');
119
- expect(phases).toContain('connected');
120
- });
123
+ expect(session.phase).toBe('connected')
124
+ expect(phases).toContain('waiting_accept')
125
+ expect(phases).toContain('connected')
126
+ })
121
127
 
122
128
  it('rejects ready.connected with missing remote', async () => {
123
- const errorHandler = vi.fn();
124
- session.on('error', errorHandler);
129
+ const errorHandler = vi.fn()
130
+ session.on('error', errorHandler)
125
131
 
126
- await session.joinFromUri(makePairingUri());
132
+ await session.joinFromUri(makePairingUri())
127
133
  transport.receive({
128
- v: 1, t: 'ready', ch: channelId,
129
- ts: Date.now(), from: '_adapter',
134
+ v: 1,
135
+ t: 'ready',
136
+ ch: channelId,
137
+ ts: Date.now(),
138
+ from: '_adapter',
130
139
  body: { state: 'connected', reconnect: false, remote: null },
131
- } as ProtocolMessage);
140
+ } as ProtocolMessage)
132
141
 
133
- expect(errorHandler).toHaveBeenCalledWith(expect.objectContaining({
134
- message: expect.stringContaining('remote does not match'),
135
- }));
136
- expect(session.phase).toBe('closed');
137
- });
138
- });
142
+ expect(errorHandler).toHaveBeenCalledWith(
143
+ expect.objectContaining({
144
+ message: expect.stringContaining('remote does not match'),
145
+ }),
146
+ )
147
+ expect(session.phase).toBe('closed')
148
+ })
149
+ })
139
150
 
140
151
  describe('request handling', () => {
141
- let dappToWalletKey: Uint8Array;
142
- let walletToDappKey: Uint8Array;
152
+ let dappToWalletKey: Uint8Array
153
+ let walletToDappKey: Uint8Array
143
154
 
144
155
  beforeEach(async () => {
145
- await session.joinFromUri(makePairingUri());
156
+ await session.joinFromUri(makePairingUri())
146
157
 
147
158
  // Derive directional keys from dApp side
148
- const walletPubB64 = transport.sent.find(m => m.t === 'join')!.from!;
149
- const walletPub = b64urlDecode(walletPubB64);
150
- const shared = computeSharedSecret(dappKp.privateKey, walletPub);
151
- const rootKey = deriveSessionKey(shared, channelId);
159
+ const walletPubB64 = transport.sent.find((m) => m.t === 'join')?.from!
160
+ const walletPub = b64urlDecode(walletPubB64)
161
+ const shared = computeSharedSecret(dappKp.privateKey, walletPub)
162
+ const rootKey = deriveSessionKey(shared, channelId)
152
163
  const context: SessionCryptoContext = {
153
164
  dappPubKeyB64: dappKp.publicKeyB64,
154
165
  walletPubKeyB64: walletPubB64,
@@ -157,100 +168,145 @@ describe('WalletSession', () => {
157
168
  events: ['accountsChanged', 'chainChanged'],
158
169
  chains: ['eip155:1'],
159
170
  },
160
- walletMeta: { name: 'Test Wallet', description: 'Test', url: 'https://test.com', icon: 'https://test.com/icon.png', address: '0xtest' },
171
+ walletMeta: {
172
+ name: 'Test Wallet',
173
+ description: 'Test',
174
+ url: 'https://test.com',
175
+ icon: 'https://test.com/icon.png',
176
+ address: '0xtest',
177
+ },
161
178
  dappName: 'Test dApp',
162
- };
163
- const keys = deriveDirectionalSessionKeys(rootKey, channelId, context);
164
- dappToWalletKey = keys.dappToWalletKey;
165
- walletToDappKey = keys.walletToDappKey;
179
+ }
180
+ const keys = deriveDirectionalSessionKeys(rootKey, channelId, context)
181
+ dappToWalletKey = keys.dappToWalletKey
182
+ walletToDappKey = keys.walletToDappKey
166
183
 
167
184
  // Connect
168
- receiveConnected();
169
- });
185
+ receiveConnected()
186
+ })
170
187
 
171
188
  it('emits request event with decrypted params', () => {
172
- const handler = vi.fn();
173
- session.on('request', handler);
189
+ const handler = vi.fn()
190
+ session.on('request', handler)
174
191
 
175
192
  // Real method inside sealed payload
176
- const sealedParams = { _method: 'wallet_signMessage', message: 'Hello World' };
193
+ const sealedParams = { _method: 'wallet_signMessage', message: 'Hello World' }
177
194
  transport.receive({
178
- v: 1, t: 'req', ch: channelId,
179
- ts: Date.now(), from: dappKp.publicKeyB64,
180
- body: { id: 'req-1', sealed: sealPayload(dappToWalletKey, channelId, 0, sealedParams, { type: 'req', from: dappKp.publicKeyB64, id: 'req-1' }) },
181
- } as ProtocolMessage);
195
+ v: 1,
196
+ t: 'req',
197
+ ch: channelId,
198
+ ts: Date.now(),
199
+ from: dappKp.publicKeyB64,
200
+ body: {
201
+ id: 'req-1',
202
+ sealed: sealPayload(dappToWalletKey, channelId, 0, sealedParams, {
203
+ type: 'req',
204
+ from: dappKp.publicKeyB64,
205
+ id: 'req-1',
206
+ }),
207
+ },
208
+ } as ProtocolMessage)
182
209
 
183
210
  expect(handler).toHaveBeenCalledWith({
184
211
  id: 'req-1',
185
212
  method: 'wallet_signMessage',
186
213
  params: { message: 'Hello World' },
187
- });
188
- });
214
+ })
215
+ })
189
216
 
190
217
  it('emits request with empty params for parameterless sealed request', () => {
191
- const handler = vi.fn();
192
- session.on('request', handler);
218
+ const handler = vi.fn()
219
+ session.on('request', handler)
193
220
 
194
- const sealedParams = { _method: 'wallet_getAccounts' };
221
+ const sealedParams = { _method: 'wallet_getAccounts' }
195
222
  transport.receive({
196
- v: 1, t: 'req', ch: channelId,
197
- ts: Date.now(), from: dappKp.publicKeyB64,
198
- body: { id: 'req-2', sealed: sealPayload(dappToWalletKey, channelId, 1, sealedParams, { type: 'req', from: dappKp.publicKeyB64, id: 'req-2' }) },
199
- } as ProtocolMessage);
223
+ v: 1,
224
+ t: 'req',
225
+ ch: channelId,
226
+ ts: Date.now(),
227
+ from: dappKp.publicKeyB64,
228
+ body: {
229
+ id: 'req-2',
230
+ sealed: sealPayload(dappToWalletKey, channelId, 1, sealedParams, {
231
+ type: 'req',
232
+ from: dappKp.publicKeyB64,
233
+ id: 'req-2',
234
+ }),
235
+ },
236
+ } as ProtocolMessage)
200
237
 
201
238
  expect(handler).toHaveBeenCalledWith({
202
239
  id: 'req-2',
203
240
  method: 'wallet_getAccounts',
204
241
  params: {},
205
- });
206
- });
242
+ })
243
+ })
207
244
 
208
245
  it('rejects unsealed request with decryption_failed', () => {
209
- const handler = vi.fn();
210
- session.on('request', handler);
246
+ const handler = vi.fn()
247
+ session.on('request', handler)
211
248
 
212
249
  transport.receive({
213
- v: 1, t: 'req', ch: channelId,
214
- ts: Date.now(), from: dappKp.publicKeyB64,
250
+ v: 1,
251
+ t: 'req',
252
+ ch: channelId,
253
+ ts: Date.now(),
254
+ from: dappKp.publicKeyB64,
215
255
  body: { id: 'req-3' },
216
- } as ProtocolMessage);
256
+ } as ProtocolMessage)
217
257
 
218
258
  // Should NOT emit request - unsealed requests are rejected
219
- expect(handler).not.toHaveBeenCalled();
259
+ expect(handler).not.toHaveBeenCalled()
220
260
  // Should send a rejection response
221
- const resMsg = transport.sent.find(m => m.t === 'res') as any;
222
- expect(resMsg).toBeTruthy();
261
+ const resMsg = transport.sent.find((m) => m.t === 'res') as any
262
+ expect(resMsg).toBeTruthy()
223
263
  // ok no longer exists on wire body
224
- });
264
+ })
225
265
 
226
266
  it('rejects sealed request missing _method with invalid_params', () => {
227
- const handler = vi.fn();
228
- session.on('request', handler);
267
+ const handler = vi.fn()
268
+ session.on('request', handler)
229
269
 
230
270
  transport.receive({
231
- v: 1, t: 'req', ch: channelId,
232
- ts: Date.now(), from: dappKp.publicKeyB64,
233
- body: { id: 'req-missing-method', sealed: sealPayload(dappToWalletKey, channelId, 0, { message: 'no method' }, { type: 'req', from: dappKp.publicKeyB64, id: 'req-missing-method' }) },
234
- } as ProtocolMessage);
235
-
236
- expect(handler).not.toHaveBeenCalled();
237
- const resMsg = transport.sent.find(m => m.t === 'res') as any;
238
- expect(resMsg).toBeTruthy();
239
- const { data } = unsealPayload(walletToDappKey, channelId, resMsg.body.sealed, { type: 'res', from: transport.sent.find(m => m.t === 'join')!.from!, id: 'req-missing-method' });
240
- expect(data).toMatchObject({ _ok: false, code: 'invalid_params' });
241
- });
242
- });
271
+ v: 1,
272
+ t: 'req',
273
+ ch: channelId,
274
+ ts: Date.now(),
275
+ from: dappKp.publicKeyB64,
276
+ body: {
277
+ id: 'req-missing-method',
278
+ sealed: sealPayload(
279
+ dappToWalletKey,
280
+ channelId,
281
+ 0,
282
+ { message: 'no method' },
283
+ { type: 'req', from: dappKp.publicKeyB64, id: 'req-missing-method' },
284
+ ),
285
+ },
286
+ } as ProtocolMessage)
287
+
288
+ expect(handler).not.toHaveBeenCalled()
289
+ const resMsg = transport.sent.find((m) => m.t === 'res') as any
290
+ expect(resMsg).toBeTruthy()
291
+ const { data } = unsealPayload(walletToDappKey, channelId, resMsg.body.sealed, {
292
+ type: 'res',
293
+ from: transport.sent.find((m) => m.t === 'join')?.from!,
294
+ id: 'req-missing-method',
295
+ })
296
+ expect(data).toMatchObject({ _ok: false, code: 'invalid_params' })
297
+ })
298
+ })
243
299
 
244
300
  describe('approve/reject', () => {
245
- let dappToWalletKey: Uint8Array;
246
- let walletToDappKey: Uint8Array;
247
- let walletPubB64: string;
301
+ let dappToWalletKey: Uint8Array
302
+ let walletToDappKey: Uint8Array
303
+ let walletPubB64: string
248
304
 
249
305
  beforeEach(async () => {
250
- await session.joinFromUri(makePairingUri());
251
- walletPubB64 = transport.sent.find(m => m.t === 'join')!.from!;
252
- const shared = computeSharedSecret(dappKp.privateKey, b64urlDecode(walletPubB64));
253
- const rootKey = deriveSessionKey(shared, channelId);
306
+ await session.joinFromUri(makePairingUri())
307
+ walletPubB64 = transport.sent.find((m) => m.t === 'join')?.from!
308
+ const shared = computeSharedSecret(dappKp.privateKey, b64urlDecode(walletPubB64))
309
+ const rootKey = deriveSessionKey(shared, channelId)
254
310
  const context: SessionCryptoContext = {
255
311
  dappPubKeyB64: dappKp.publicKeyB64,
256
312
  walletPubKeyB64: walletPubB64,
@@ -259,130 +315,232 @@ describe('WalletSession', () => {
259
315
  events: ['accountsChanged', 'chainChanged'],
260
316
  chains: ['eip155:1'],
261
317
  },
262
- walletMeta: { name: 'Test Wallet', description: 'Test', url: 'https://test.com', icon: 'https://test.com/icon.png', address: '0xtest' },
318
+ walletMeta: {
319
+ name: 'Test Wallet',
320
+ description: 'Test',
321
+ url: 'https://test.com',
322
+ icon: 'https://test.com/icon.png',
323
+ address: '0xtest',
324
+ },
263
325
  dappName: 'Test dApp',
264
- };
265
- const keys = deriveDirectionalSessionKeys(rootKey, channelId, context);
266
- dappToWalletKey = keys.dappToWalletKey;
267
- walletToDappKey = keys.walletToDappKey;
326
+ }
327
+ const keys = deriveDirectionalSessionKeys(rootKey, channelId, context)
328
+ dappToWalletKey = keys.dappToWalletKey
329
+ walletToDappKey = keys.walletToDappKey
268
330
 
269
- receiveConnected();
270
- });
331
+ receiveConnected()
332
+ })
271
333
 
272
334
  it('approve sends encrypted ok response', () => {
273
335
  transport.receive({
274
- v: 1, t: 'req', ch: channelId,
275
- ts: Date.now(), from: dappKp.publicKeyB64,
276
- body: { id: 'req-1', sealed: sealPayload(dappToWalletKey, channelId, 0, { _method: 'wallet_getAccounts' }, { type: 'req', from: dappKp.publicKeyB64, id: 'req-1' }) },
277
- } as ProtocolMessage);
336
+ v: 1,
337
+ t: 'req',
338
+ ch: channelId,
339
+ ts: Date.now(),
340
+ from: dappKp.publicKeyB64,
341
+ body: {
342
+ id: 'req-1',
343
+ sealed: sealPayload(
344
+ dappToWalletKey,
345
+ channelId,
346
+ 0,
347
+ { _method: 'wallet_getAccounts' },
348
+ { type: 'req', from: dappKp.publicKeyB64, id: 'req-1' },
349
+ ),
350
+ },
351
+ } as ProtocolMessage)
278
352
 
279
- session.approve('req-1', ['0xabc123']);
353
+ session.approve('req-1', ['0xabc123'])
280
354
 
281
- const resMsg = transport.sent.find(m => m.t === 'res') as any;
282
- expect(resMsg).toBeTruthy();
283
- expect(resMsg.body.id).toBe('req-1');
284
- expect(resMsg.body.sealed).toBeTruthy();
355
+ const resMsg = transport.sent.find((m) => m.t === 'res') as any
356
+ expect(resMsg).toBeTruthy()
357
+ expect(resMsg.body.id).toBe('req-1')
358
+ expect(resMsg.body.sealed).toBeTruthy()
285
359
 
286
360
  // Verify dApp can decrypt the response (wallet->dApp uses walletToDappKey)
287
- const { data } = unsealPayload(walletToDappKey, channelId, resMsg.body.sealed, { type: 'res', from: walletPubB64, id: 'req-1' });
288
- expect(data).toEqual({ _ok: true, _result: ['0xabc123'] });
289
- });
361
+ const { data } = unsealPayload(walletToDappKey, channelId, resMsg.body.sealed, {
362
+ type: 'res',
363
+ from: walletPubB64,
364
+ id: 'req-1',
365
+ })
366
+ expect(data).toEqual({ _ok: true, _result: ['0xabc123'] })
367
+ })
290
368
 
291
369
  it('reject sends encrypted error response', () => {
292
370
  transport.receive({
293
- v: 1, t: 'req', ch: channelId,
294
- ts: Date.now(), from: dappKp.publicKeyB64,
295
- body: { id: 'req-2', sealed: sealPayload(dappToWalletKey, channelId, 0, { _method: 'wallet_signMessage', message: 'test' }, { type: 'req', from: dappKp.publicKeyB64, id: 'req-2' }) },
296
- } as ProtocolMessage);
371
+ v: 1,
372
+ t: 'req',
373
+ ch: channelId,
374
+ ts: Date.now(),
375
+ from: dappKp.publicKeyB64,
376
+ body: {
377
+ id: 'req-2',
378
+ sealed: sealPayload(
379
+ dappToWalletKey,
380
+ channelId,
381
+ 0,
382
+ { _method: 'wallet_signMessage', message: 'test' },
383
+ { type: 'req', from: dappKp.publicKeyB64, id: 'req-2' },
384
+ ),
385
+ },
386
+ } as ProtocolMessage)
297
387
 
298
- session.reject('req-2', 'user_rejected', 'User said no');
388
+ session.reject('req-2', 'user_rejected', 'User said no')
299
389
 
300
- const resMsg = transport.sent.find(m => m.t === 'res') as any;
301
- expect(resMsg).toBeTruthy();
302
- expect(resMsg.body.id).toBe('req-2');
303
- expect(resMsg.body.sealed).toBeTruthy();
390
+ const resMsg = transport.sent.find((m) => m.t === 'res') as any
391
+ expect(resMsg).toBeTruthy()
392
+ expect(resMsg.body.id).toBe('req-2')
393
+ expect(resMsg.body.sealed).toBeTruthy()
304
394
 
305
- const { data } = unsealPayload(walletToDappKey, channelId, resMsg.body.sealed, { type: 'res', from: walletPubB64, id: 'req-2' });
306
- expect(data).toEqual({ _ok: false, code: 'user_rejected', message: 'User said no' });
307
- });
395
+ const { data } = unsealPayload(walletToDappKey, channelId, resMsg.body.sealed, {
396
+ type: 'res',
397
+ from: walletPubB64,
398
+ id: 'req-2',
399
+ })
400
+ expect(data).toEqual({ _ok: false, code: 'user_rejected', message: 'User said no' })
401
+ })
308
402
 
309
403
  it('approve increments send sequence', () => {
310
404
  for (let i = 0; i < 3; i++) {
311
405
  transport.receive({
312
- v: 1, t: 'req', ch: channelId,
313
- ts: Date.now(), from: dappKp.publicKeyB64,
314
- body: { id: `req-${i}`, sealed: sealPayload(dappToWalletKey, channelId, i, { _method: 'wallet_getAccounts' }, { type: 'req', from: dappKp.publicKeyB64, id: `req-${i}` }) },
315
- } as ProtocolMessage);
316
- session.approve(`req-${i}`, ['0x123']);
406
+ v: 1,
407
+ t: 'req',
408
+ ch: channelId,
409
+ ts: Date.now(),
410
+ from: dappKp.publicKeyB64,
411
+ body: {
412
+ id: `req-${i}`,
413
+ sealed: sealPayload(
414
+ dappToWalletKey,
415
+ channelId,
416
+ i,
417
+ { _method: 'wallet_getAccounts' },
418
+ { type: 'req', from: dappKp.publicKeyB64, id: `req-${i}` },
419
+ ),
420
+ },
421
+ } as ProtocolMessage)
422
+ session.approve(`req-${i}`, ['0x123'])
317
423
  }
318
424
 
319
425
  // All 3 responses should have different sealed payloads (different seqs)
320
- const responses = transport.sent.filter(m => m.t === 'res') as any[];
321
- expect(responses).toHaveLength(3);
322
- const sealedSet = new Set(responses.map((r: any) => r.body.sealed));
323
- expect(sealedSet.size).toBe(3);
324
- });
426
+ const responses = transport.sent.filter((m) => m.t === 'res') as any[]
427
+ expect(responses).toHaveLength(3)
428
+ const sealedSet = new Set(responses.map((r: any) => r.body.sealed))
429
+ expect(sealedSet.size).toBe(3)
430
+ })
325
431
 
326
432
  it('re-encrypts cached response for duplicate request id with same params', () => {
327
- const handler = vi.fn();
328
- session.on('request', handler);
329
- const requestPayload = { _method: 'wallet_getAccounts' };
433
+ const handler = vi.fn()
434
+ session.on('request', handler)
435
+ const requestPayload = { _method: 'wallet_getAccounts' }
330
436
 
331
437
  transport.receive({
332
- v: 1, t: 'req', ch: channelId,
333
- ts: Date.now(), from: dappKp.publicKeyB64,
334
- body: { id: 'dup-1', sealed: sealPayload(dappToWalletKey, channelId, 0, requestPayload, { type: 'req', from: dappKp.publicKeyB64, id: 'dup-1' }) },
335
- } as ProtocolMessage);
336
- session.approve('dup-1', ['0xabc123']);
438
+ v: 1,
439
+ t: 'req',
440
+ ch: channelId,
441
+ ts: Date.now(),
442
+ from: dappKp.publicKeyB64,
443
+ body: {
444
+ id: 'dup-1',
445
+ sealed: sealPayload(dappToWalletKey, channelId, 0, requestPayload, {
446
+ type: 'req',
447
+ from: dappKp.publicKeyB64,
448
+ id: 'dup-1',
449
+ }),
450
+ },
451
+ } as ProtocolMessage)
452
+ session.approve('dup-1', ['0xabc123'])
337
453
 
338
454
  transport.receive({
339
- v: 1, t: 'req', ch: channelId,
340
- ts: Date.now(), from: dappKp.publicKeyB64,
341
- body: { id: 'dup-1', sealed: sealPayload(dappToWalletKey, channelId, 1, requestPayload, { type: 'req', from: dappKp.publicKeyB64, id: 'dup-1' }) },
342
- } as ProtocolMessage);
343
-
344
- expect(handler).toHaveBeenCalledTimes(1);
345
- const responses = transport.sent.filter(m => m.t === 'res') as any[];
346
- expect(responses).toHaveLength(2);
347
- expect(responses[0]!.body.sealed).not.toBe(responses[1]!.body.sealed);
348
- const { data } = unsealPayload(walletToDappKey, channelId, responses[1]!.body.sealed, { type: 'res', from: walletPubB64, id: 'dup-1' });
349
- expect(data).toEqual({ _ok: true, _result: ['0xabc123'] });
350
- });
455
+ v: 1,
456
+ t: 'req',
457
+ ch: channelId,
458
+ ts: Date.now(),
459
+ from: dappKp.publicKeyB64,
460
+ body: {
461
+ id: 'dup-1',
462
+ sealed: sealPayload(dappToWalletKey, channelId, 1, requestPayload, {
463
+ type: 'req',
464
+ from: dappKp.publicKeyB64,
465
+ id: 'dup-1',
466
+ }),
467
+ },
468
+ } as ProtocolMessage)
469
+
470
+ expect(handler).toHaveBeenCalledTimes(1)
471
+ const responses = transport.sent.filter((m) => m.t === 'res') as any[]
472
+ expect(responses).toHaveLength(2)
473
+ expect(responses[0]?.body.sealed).not.toBe(responses[1]?.body.sealed)
474
+ const { data } = unsealPayload(walletToDappKey, channelId, responses[1]?.body.sealed, {
475
+ type: 'res',
476
+ from: walletPubB64,
477
+ id: 'dup-1',
478
+ })
479
+ expect(data).toEqual({ _ok: true, _result: ['0xabc123'] })
480
+ })
351
481
 
352
482
  it('rejects duplicate request id with different params', () => {
353
- const handler = vi.fn();
354
- session.on('request', handler);
483
+ const handler = vi.fn()
484
+ session.on('request', handler)
355
485
 
356
486
  transport.receive({
357
- v: 1, t: 'req', ch: channelId,
358
- ts: Date.now(), from: dappKp.publicKeyB64,
359
- body: { id: 'dup-2', sealed: sealPayload(dappToWalletKey, channelId, 0, { _method: 'wallet_getAccounts' }, { type: 'req', from: dappKp.publicKeyB64, id: 'dup-2' }) },
360
- } as ProtocolMessage);
361
- session.approve('dup-2', ['0xabc123']);
487
+ v: 1,
488
+ t: 'req',
489
+ ch: channelId,
490
+ ts: Date.now(),
491
+ from: dappKp.publicKeyB64,
492
+ body: {
493
+ id: 'dup-2',
494
+ sealed: sealPayload(
495
+ dappToWalletKey,
496
+ channelId,
497
+ 0,
498
+ { _method: 'wallet_getAccounts' },
499
+ { type: 'req', from: dappKp.publicKeyB64, id: 'dup-2' },
500
+ ),
501
+ },
502
+ } as ProtocolMessage)
503
+ session.approve('dup-2', ['0xabc123'])
362
504
 
363
505
  transport.receive({
364
- v: 1, t: 'req', ch: channelId,
365
- ts: Date.now(), from: dappKp.publicKeyB64,
366
- body: { id: 'dup-2', sealed: sealPayload(dappToWalletKey, channelId, 1, { _method: 'wallet_getAccounts', changed: true }, { type: 'req', from: dappKp.publicKeyB64, id: 'dup-2' }) },
367
- } as ProtocolMessage);
368
-
369
- expect(handler).toHaveBeenCalledTimes(1);
370
- const responses = transport.sent.filter(m => m.t === 'res') as any[];
371
- const duplicateResponse = responses[1]!;
372
- const { data } = unsealPayload(walletToDappKey, channelId, duplicateResponse.body.sealed, { type: 'res', from: walletPubB64, id: 'dup-2' });
373
- expect(data).toMatchObject({ _ok: false, code: 'invalid_params' });
374
- });
375
- });
506
+ v: 1,
507
+ t: 'req',
508
+ ch: channelId,
509
+ ts: Date.now(),
510
+ from: dappKp.publicKeyB64,
511
+ body: {
512
+ id: 'dup-2',
513
+ sealed: sealPayload(
514
+ dappToWalletKey,
515
+ channelId,
516
+ 1,
517
+ { _method: 'wallet_getAccounts', changed: true },
518
+ { type: 'req', from: dappKp.publicKeyB64, id: 'dup-2' },
519
+ ),
520
+ },
521
+ } as ProtocolMessage)
522
+
523
+ expect(handler).toHaveBeenCalledTimes(1)
524
+ const responses = transport.sent.filter((m) => m.t === 'res') as any[]
525
+ const duplicateResponse = responses[1]!
526
+ const { data } = unsealPayload(walletToDappKey, channelId, duplicateResponse.body.sealed, {
527
+ type: 'res',
528
+ from: walletPubB64,
529
+ id: 'dup-2',
530
+ })
531
+ expect(data).toMatchObject({ _ok: false, code: 'invalid_params' })
532
+ })
533
+ })
376
534
 
377
535
  describe('pushEvent', () => {
378
- let walletToDappKey: Uint8Array;
379
- let walletPubB64: string;
536
+ let walletToDappKey: Uint8Array
537
+ let walletPubB64: string
380
538
 
381
539
  beforeEach(async () => {
382
- await session.joinFromUri(makePairingUri());
383
- walletPubB64 = transport.sent.find(m => m.t === 'join')!.from!;
384
- const shared = computeSharedSecret(dappKp.privateKey, b64urlDecode(walletPubB64));
385
- const rootKey = deriveSessionKey(shared, channelId);
540
+ await session.joinFromUri(makePairingUri())
541
+ walletPubB64 = transport.sent.find((m) => m.t === 'join')?.from!
542
+ const shared = computeSharedSecret(dappKp.privateKey, b64urlDecode(walletPubB64))
543
+ const rootKey = deriveSessionKey(shared, channelId)
386
544
  const context: SessionCryptoContext = {
387
545
  dappPubKeyB64: dappKp.publicKeyB64,
388
546
  walletPubKeyB64: walletPubB64,
@@ -391,90 +549,113 @@ describe('WalletSession', () => {
391
549
  events: ['accountsChanged', 'chainChanged'],
392
550
  chains: ['eip155:1'],
393
551
  },
394
- walletMeta: { name: 'Test Wallet', description: 'Test', url: 'https://test.com', icon: 'https://test.com/icon.png', address: '0xtest' },
552
+ walletMeta: {
553
+ name: 'Test Wallet',
554
+ description: 'Test',
555
+ url: 'https://test.com',
556
+ icon: 'https://test.com/icon.png',
557
+ address: '0xtest',
558
+ },
395
559
  dappName: 'Test dApp',
396
- };
397
- const keys = deriveDirectionalSessionKeys(rootKey, channelId, context);
398
- walletToDappKey = keys.walletToDappKey;
560
+ }
561
+ const keys = deriveDirectionalSessionKeys(rootKey, channelId, context)
562
+ walletToDappKey = keys.walletToDappKey
399
563
 
400
- receiveConnected();
401
- });
564
+ receiveConnected()
565
+ })
402
566
 
403
567
  it('sends encrypted event message', () => {
404
- session.pushEvent('accountsChanged', { accounts: ['0xnew'] });
568
+ session.pushEvent('accountsChanged', { accounts: ['0xnew'] })
405
569
 
406
- const evtMsg = transport.sent.find(m => m.t === 'evt') as any;
407
- expect(evtMsg).toBeTruthy();
408
- expect(evtMsg.body.id).toBeTruthy();
409
- expect(evtMsg.body.sealed).toBeTruthy();
570
+ const evtMsg = transport.sent.find((m) => m.t === 'evt') as any
571
+ expect(evtMsg).toBeTruthy()
572
+ expect(evtMsg.body.id).toBeTruthy()
573
+ expect(evtMsg.body.sealed).toBeTruthy()
410
574
 
411
575
  // Verify dApp can decrypt (wallet->dApp uses walletToDappKey)
412
- const { data } = unsealPayload(walletToDappKey, channelId, evtMsg.body.sealed, { type: 'evt', from: walletPubB64, id: evtMsg.body.id });
576
+ const { data } = unsealPayload(walletToDappKey, channelId, evtMsg.body.sealed, {
577
+ type: 'evt',
578
+ from: walletPubB64,
579
+ id: evtMsg.body.id,
580
+ })
413
581
  // Real event name and data are inside sealed payload
414
- expect(data).toEqual({ _event: 'accountsChanged', accounts: ['0xnew'] });
415
- });
582
+ expect(data).toEqual({ _event: 'accountsChanged', accounts: ['0xnew'] })
583
+ })
416
584
 
417
585
  it('does nothing when not connected', () => {
418
586
  const idleSession = new WalletSession({
419
587
  transport: new MockTransport(),
420
588
  capabilities: { methods: [], events: [], chains: [] },
421
- meta: { name: 'Test Wallet', description: 'Test', url: 'https://wallet.test', icon: 'https://wallet.test/icon.png' },
422
- });
423
- idleSession.pushEvent('test', {});
589
+ meta: {
590
+ name: 'Test Wallet',
591
+ description: 'Test',
592
+ url: 'https://wallet.test',
593
+ icon: 'https://wallet.test/icon.png',
594
+ },
595
+ })
596
+ idleSession.pushEvent('test', {})
424
597
  // Should not throw, just no-op
425
- });
598
+ })
426
599
 
427
600
  it('sends chainChanged event', () => {
428
- session.pushEvent('chainChanged', { chainId: 'eip155:137' });
429
-
430
- const evtMsg = transport.sent.find(m => m.t === 'evt') as any;
431
- const { data } = unsealPayload(walletToDappKey, channelId, evtMsg.body.sealed, { type: 'evt', from: walletPubB64, id: evtMsg.body.id });
432
- expect(data).toEqual({ _event: 'chainChanged', chainId: 'eip155:137' });
433
- });
434
- });
601
+ session.pushEvent('chainChanged', { chainId: 'eip155:137' })
602
+
603
+ const evtMsg = transport.sent.find((m) => m.t === 'evt') as any
604
+ const { data } = unsealPayload(walletToDappKey, channelId, evtMsg.body.sealed, {
605
+ type: 'evt',
606
+ from: walletPubB64,
607
+ id: evtMsg.body.id,
608
+ })
609
+ expect(data).toEqual({ _event: 'chainChanged', chainId: 'eip155:137' })
610
+ })
611
+ })
435
612
 
436
613
  describe('ping/pong', () => {
437
614
  beforeEach(async () => {
438
- await session.joinFromUri(makePairingUri());
439
- receiveConnected();
440
- });
615
+ await session.joinFromUri(makePairingUri())
616
+ receiveConnected()
617
+ })
441
618
 
442
619
  it('responds to ping with pong', () => {
443
620
  transport.receive({
444
- v: 1, t: 'ping', ch: channelId,
445
- ts: 12345, from: dappKp.publicKeyB64, body: {},
446
- } as ProtocolMessage);
447
-
448
- const pong = transport.sent.find(m => m.t === 'pong');
449
- expect(pong).toBeTruthy();
450
- });
621
+ v: 1,
622
+ t: 'ping',
623
+ ch: channelId,
624
+ ts: 12345,
625
+ from: dappKp.publicKeyB64,
626
+ body: {},
627
+ } as ProtocolMessage)
628
+
629
+ const pong = transport.sent.find((m) => m.t === 'pong')
630
+ expect(pong).toBeTruthy()
631
+ })
451
632
 
452
633
  it('sends ping', () => {
453
- session.ping();
454
- const ping = transport.sent.find(m => m.t === 'ping');
455
- expect(ping).toBeTruthy();
456
- });
457
- });
634
+ session.ping()
635
+ const ping = transport.sent.find((m) => m.t === 'ping')
636
+ expect(ping).toBeTruthy()
637
+ })
638
+ })
458
639
 
459
640
  describe('close', () => {
460
641
  it('sends close and transitions to closed', async () => {
461
- await session.joinFromUri(makePairingUri());
462
- session.close();
642
+ await session.joinFromUri(makePairingUri())
643
+ session.close()
463
644
 
464
- const closeMsg = transport.sent.find(m => m.t === 'close');
465
- expect(closeMsg).toBeTruthy();
466
- expect((closeMsg as any).body.reason).toBe('normal');
467
- expect(session.phase).toBe('closed');
468
- });
469
- });
645
+ const closeMsg = transport.sent.find((m) => m.t === 'close')
646
+ expect(closeMsg).toBeTruthy()
647
+ expect((closeMsg as any).body.reason).toBe('normal')
648
+ expect(session.phase).toBe('closed')
649
+ })
650
+ })
470
651
 
471
652
  describe('serialize/restore', () => {
472
653
  it('round-trips session state', async () => {
473
- await session.joinFromUri(makePairingUri());
654
+ await session.joinFromUri(makePairingUri())
474
655
 
475
- receiveConnected();
656
+ receiveConnected()
476
657
 
477
- const json = session.serialize();
658
+ const json = session.serialize()
478
659
  const newSession = new WalletSession({
479
660
  transport: new MockTransport(),
480
661
  capabilities: {
@@ -482,115 +663,138 @@ describe('WalletSession', () => {
482
663
  events: ['accountsChanged', 'chainChanged'],
483
664
  chains: ['eip155:1'],
484
665
  },
485
- meta: { name: 'Test Wallet', description: 'Test', url: 'https://test.com', icon: 'https://test.com/icon.png', address: '0xtest' },
486
- });
487
- expect(newSession.restore(json)).toBe(true);
488
- expect(newSession.channelId).toBe(channelId);
489
- });
666
+ meta: {
667
+ name: 'Test Wallet',
668
+ description: 'Test',
669
+ url: 'https://test.com',
670
+ icon: 'https://test.com/icon.png',
671
+ address: '0xtest',
672
+ },
673
+ })
674
+ expect(newSession.restore(json)).toBe(true)
675
+ expect(newSession.channelId).toBe(channelId)
676
+ })
490
677
 
491
678
  it('rejects restore when capabilities no longer match the transcript', async () => {
492
- await session.joinFromUri(makePairingUri());
679
+ await session.joinFromUri(makePairingUri())
493
680
 
494
- const json = session.serialize();
681
+ const json = session.serialize()
495
682
  const newSession = new WalletSession({
496
683
  transport: new MockTransport(),
497
684
  capabilities: { methods: ['wallet_getAccounts'], events: [], chains: ['eip155:1'] },
498
- meta: { name: 'Test Wallet', description: 'Test', url: 'https://test.com', icon: 'https://test.com/icon.png', address: '0xtest' },
499
- });
685
+ meta: {
686
+ name: 'Test Wallet',
687
+ description: 'Test',
688
+ url: 'https://test.com',
689
+ icon: 'https://test.com/icon.png',
690
+ address: '0xtest',
691
+ },
692
+ })
500
693
 
501
- expect(newSession.restore(json)).toBe(false);
502
- });
694
+ expect(newSession.restore(json)).toBe(false)
695
+ })
503
696
 
504
697
  it('returns false for invalid JSON', () => {
505
- expect(session.restore('invalid')).toBe(false);
506
- expect(session.restore('{}')).toBe(false);
507
- });
508
- });
698
+ expect(session.restore('invalid')).toBe(false)
699
+ expect(session.restore('{}')).toBe(false)
700
+ })
701
+ })
509
702
 
510
703
  describe('close message handling', () => {
511
704
  it('transitions to closed on close message from dApp', async () => {
512
- await session.joinFromUri(makePairingUri());
513
- receiveConnected();
705
+ await session.joinFromUri(makePairingUri())
706
+ receiveConnected()
514
707
 
515
708
  transport.receive({
516
- v: 1, t: 'close', ch: channelId,
517
- ts: Date.now(), from: dappKp.publicKeyB64,
709
+ v: 1,
710
+ t: 'close',
711
+ ch: channelId,
712
+ ts: Date.now(),
713
+ from: dappKp.publicKeyB64,
518
714
  body: { reason: 'normal' },
519
- } as ProtocolMessage);
715
+ } as ProtocolMessage)
520
716
 
521
- expect(session.phase).toBe('closed');
522
- });
523
- });
717
+ expect(session.phase).toBe('closed')
718
+ })
719
+ })
524
720
 
525
721
  describe('destroy', () => {
526
722
  it('closes and removes all listeners', async () => {
527
- await session.joinFromUri(makePairingUri());
528
- const handler = vi.fn();
529
- session.on('phase', handler);
530
- session.destroy();
531
- expect(session.phase).toBe('closed');
532
- });
533
- });
723
+ await session.joinFromUri(makePairingUri())
724
+ const handler = vi.fn()
725
+ session.on('phase', handler)
726
+ session.destroy()
727
+ expect(session.phase).toBe('closed')
728
+ })
729
+ })
534
730
 
535
731
  describe('session fingerprint after prepareJoin', () => {
536
732
  it('sessionFingerprint is set and event was emitted after prepareJoin', () => {
537
- const fpHandler = vi.fn();
538
- session.on('sessionFingerprint', fpHandler);
733
+ const fpHandler = vi.fn()
734
+ session.on('sessionFingerprint', fpHandler)
539
735
 
540
- const uri = makePairingUri();
541
- const fingerprint = session.prepareJoin(uri);
736
+ const uri = makePairingUri()
737
+ const fingerprint = session.prepareJoin(uri)
542
738
 
543
- expect(session.sessionFingerprint).toMatch(/^\d{4}$/);
544
- expect(fingerprint).toBe(session.sessionFingerprint);
545
- expect(fpHandler).toHaveBeenCalledTimes(1);
546
- expect(fpHandler).toHaveBeenCalledWith(session.sessionFingerprint);
547
- });
548
- });
739
+ expect(session.sessionFingerprint).toMatch(/^\d{4}$/)
740
+ expect(fingerprint).toBe(session.sessionFingerprint)
741
+ expect(fpHandler).toHaveBeenCalledTimes(1)
742
+ expect(fpHandler).toHaveBeenCalledWith(session.sessionFingerprint)
743
+ })
744
+ })
549
745
 
550
746
  describe('protocol compliance', () => {
551
747
  it('rejects messages with from="_adapter" for peer types (§2)', async () => {
552
- const errorHandler = vi.fn();
553
- session.on('error', errorHandler);
748
+ const errorHandler = vi.fn()
749
+ session.on('error', errorHandler)
554
750
 
555
- await session.joinFromUri(makePairingUri());
556
- receiveConnected();
751
+ await session.joinFromUri(makePairingUri())
752
+ receiveConnected()
557
753
 
558
754
  // Send a req message with from: '_adapter' — should be rejected
559
755
  transport.receive({
560
- v: 1, t: 'req', ch: channelId,
561
- ts: Date.now(), from: '_adapter',
756
+ v: 1,
757
+ t: 'req',
758
+ ch: channelId,
759
+ ts: Date.now(),
760
+ from: '_adapter',
562
761
  body: { id: 'spoofed-1', sealed: 'fake' },
563
- } as ProtocolMessage);
762
+ } as ProtocolMessage)
564
763
 
565
- expect(errorHandler).toHaveBeenCalledWith(expect.objectContaining({
566
- message: expect.stringContaining('_adapter'),
567
- }));
568
- });
764
+ expect(errorHandler).toHaveBeenCalledWith(
765
+ expect.objectContaining({
766
+ message: expect.stringContaining('_adapter'),
767
+ }),
768
+ )
769
+ })
569
770
 
570
771
  it('rejects messages with unsupported version (§15 rule 12)', async () => {
571
- await session.joinFromUri(makePairingUri());
572
- receiveConnected();
772
+ await session.joinFromUri(makePairingUri())
773
+ receiveConnected()
573
774
 
574
775
  // Send a message with v: 2 — should close with unsupported_version
575
776
  transport.receive({
576
- v: 2, t: 'req', ch: channelId,
577
- ts: Date.now(), from: dappKp.publicKeyB64,
777
+ v: 2,
778
+ t: 'req',
779
+ ch: channelId,
780
+ ts: Date.now(),
781
+ from: dappKp.publicKeyB64,
578
782
  body: { id: 'v2-req', sealed: 'fake' },
579
- } as unknown as ProtocolMessage);
783
+ } as unknown as ProtocolMessage)
580
784
 
581
- expect(session.phase).toBe('closed');
582
- const closeMsg = transport.sent.find(m => m.t === 'close') as any;
583
- expect(closeMsg).toBeTruthy();
584
- expect(closeMsg.body.reason).toBe('unsupported_version');
585
- });
785
+ expect(session.phase).toBe('closed')
786
+ const closeMsg = transport.sent.find((m) => m.t === 'close') as any
787
+ expect(closeMsg).toBeTruthy()
788
+ expect(closeMsg.body.reason).toBe('unsupported_version')
789
+ })
586
790
 
587
791
  it('rejects unsupported methods at runtime (§7.1)', async () => {
588
792
  // Session has capabilities.methods = ['wallet_getAccounts', 'wallet_signMessage']
589
- await session.joinFromUri(makePairingUri());
793
+ await session.joinFromUri(makePairingUri())
590
794
 
591
- const walletPubB64 = transport.sent.find(m => m.t === 'join')!.from!;
592
- const shared = computeSharedSecret(dappKp.privateKey, b64urlDecode(walletPubB64));
593
- const rootKey = deriveSessionKey(shared, channelId);
795
+ const walletPubB64 = transport.sent.find((m) => m.t === 'join')?.from!
796
+ const shared = computeSharedSecret(dappKp.privateKey, b64urlDecode(walletPubB64))
797
+ const rootKey = deriveSessionKey(shared, channelId)
594
798
  const context: SessionCryptoContext = {
595
799
  dappPubKeyB64: dappKp.publicKeyB64,
596
800
  walletPubKeyB64: walletPubB64,
@@ -599,40 +803,60 @@ describe('WalletSession', () => {
599
803
  events: ['accountsChanged', 'chainChanged'],
600
804
  chains: ['eip155:1'],
601
805
  },
602
- walletMeta: { name: 'Test Wallet', description: 'Test', url: 'https://test.com', icon: 'https://test.com/icon.png', address: '0xtest' },
806
+ walletMeta: {
807
+ name: 'Test Wallet',
808
+ description: 'Test',
809
+ url: 'https://test.com',
810
+ icon: 'https://test.com/icon.png',
811
+ address: '0xtest',
812
+ },
603
813
  dappName: 'Test dApp',
604
- };
605
- const keys = deriveDirectionalSessionKeys(rootKey, channelId, context);
606
- const dappToWalletKey = keys.dappToWalletKey;
607
- const walletToDappKey = keys.walletToDappKey;
814
+ }
815
+ const keys = deriveDirectionalSessionKeys(rootKey, channelId, context)
816
+ const dappToWalletKey = keys.dappToWalletKey
817
+ const walletToDappKey = keys.walletToDappKey
608
818
 
609
- receiveConnected();
819
+ receiveConnected()
610
820
 
611
- const handler = vi.fn();
612
- session.on('request', handler);
821
+ const handler = vi.fn()
822
+ session.on('request', handler)
613
823
 
614
824
  // Send a request for a method NOT in capabilities
615
- const sealedParams = { _method: 'wallet_signTransaction', data: '0x...' };
825
+ const sealedParams = { _method: 'wallet_signTransaction', data: '0x...' }
616
826
  transport.receive({
617
- v: 1, t: 'req', ch: channelId,
618
- ts: Date.now(), from: dappKp.publicKeyB64,
619
- body: { id: 'unsup-1', sealed: sealPayload(dappToWalletKey, channelId, 0, sealedParams, { type: 'req', from: dappKp.publicKeyB64, id: 'unsup-1' }) },
620
- } as ProtocolMessage);
827
+ v: 1,
828
+ t: 'req',
829
+ ch: channelId,
830
+ ts: Date.now(),
831
+ from: dappKp.publicKeyB64,
832
+ body: {
833
+ id: 'unsup-1',
834
+ sealed: sealPayload(dappToWalletKey, channelId, 0, sealedParams, {
835
+ type: 'req',
836
+ from: dappKp.publicKeyB64,
837
+ id: 'unsup-1',
838
+ }),
839
+ },
840
+ } as ProtocolMessage)
621
841
 
622
842
  // Should NOT emit request
623
- expect(handler).not.toHaveBeenCalled();
843
+ expect(handler).not.toHaveBeenCalled()
624
844
 
625
845
  // Should send an error response with unsupported_method
626
- const resMsg = transport.sent.find(m => m.t === 'res') as any;
627
- expect(resMsg).toBeTruthy();
628
- const { data } = unsealPayload(walletToDappKey, channelId, resMsg.body.sealed, { type: 'res', from: walletPubB64, id: 'unsup-1' });
629
- expect(data).toMatchObject({ _ok: false, code: 'unsupported_method' });
630
- });
631
- });
846
+ const resMsg = transport.sent.find((m) => m.t === 'res') as any
847
+ expect(resMsg).toBeTruthy()
848
+ const { data } = unsealPayload(walletToDappKey, channelId, resMsg.body.sealed, {
849
+ type: 'res',
850
+ from: walletPubB64,
851
+ id: 'unsup-1',
852
+ })
853
+ expect(data).toMatchObject({ _ok: false, code: 'unsupported_method' })
854
+ })
855
+ })
632
856
 
633
857
  describe('scope intersection (computeScopeIntersection)', () => {
634
858
  it('intersects wallet capabilities with dApp-declared scope from URI', async () => {
635
- const wideWalletTransport = new MockTransport();
859
+ const wideWalletTransport = new MockTransport()
636
860
  const wideWalletSession = new WalletSession({
637
861
  transport: wideWalletTransport,
638
862
  capabilities: {
@@ -641,11 +865,11 @@ describe('WalletSession', () => {
641
865
  chains: ['eip155:1', 'eip155:137'],
642
866
  },
643
867
  meta: { name: 'W', description: 'W', url: 'https://w.com', icon: 'https://w.com/i.png' },
644
- });
868
+ })
645
869
 
646
870
  // Build a URI that declares only methods=a,b and chains=eip155:1
647
- const dappKpLocal = generateX25519KeyPair();
648
- const chLocal = generateChannelId();
871
+ const dappKpLocal = generateX25519KeyPair()
872
+ const chLocal = generateChannelId()
649
873
  const uri = buildPairingUri({
650
874
  channelId: chLocal,
651
875
  pubkeyB64: dappKpLocal.publicKeyB64,
@@ -655,29 +879,33 @@ describe('WalletSession', () => {
655
879
  icon: 'https://test.com/icon.png',
656
880
  methods: ['a', 'b'],
657
881
  chains: ['eip155:1'],
658
- });
882
+ })
659
883
 
660
- await wideWalletSession.joinFromUri(uri);
884
+ await wideWalletSession.joinFromUri(uri)
661
885
 
662
886
  // The join message should contain sealed_join with intersected capabilities
663
- const joinMsg = wideWalletTransport.sent.find(m => m.t === 'join') as any;
664
- expect(joinMsg).toBeTruthy();
665
- expect(joinMsg.body.sealed_join).toBeTruthy();
887
+ const joinMsg = wideWalletTransport.sent.find((m) => m.t === 'join') as any
888
+ expect(joinMsg).toBeTruthy()
889
+ expect(joinMsg.body.sealed_join).toBeTruthy()
666
890
 
667
891
  // Unseal the join to verify effective capabilities
668
- const walletPubB64 = joinMsg.from!;
669
- const walletPub = b64urlDecode(walletPubB64);
670
- const shared = computeSharedSecret(dappKpLocal.privateKey, walletPub);
671
- const rootKey = deriveSessionKey(shared, chLocal);
672
- const joinKey = deriveJoinEncryptionKey(rootKey, chLocal);
673
- const unsealed = unsealJoin(joinKey, chLocal, joinMsg.body.sealed_join);
674
-
675
- const caps = unsealed.capabilities as { methods: string[]; chains: string[]; events: string[] };
892
+ const walletPubB64 = joinMsg.from!
893
+ const walletPub = b64urlDecode(walletPubB64)
894
+ const shared = computeSharedSecret(dappKpLocal.privateKey, walletPub)
895
+ const rootKey = deriveSessionKey(shared, chLocal)
896
+ const joinKey = deriveJoinEncryptionKey(rootKey, chLocal)
897
+ const unsealed = unsealJoin(joinKey, chLocal, joinMsg.body.sealed_join)
898
+
899
+ const caps = unsealed.capabilities as {
900
+ methods: string[]
901
+ chains: string[]
902
+ events: string[]
903
+ }
676
904
  // Wallet grants ALL its capabilities (not just the intersection)
677
905
  // per §7.1: wallet MAY grant additional methods/chains beyond requested
678
- expect(caps.methods).toEqual(['a', 'b', 'c']);
679
- expect(caps.chains).toEqual(['eip155:1', 'eip155:137']);
680
- expect(caps.events).toEqual(['accountsChanged']);
681
- });
682
- });
683
- });
906
+ expect(caps.methods).toEqual(['a', 'b', 'c'])
907
+ expect(caps.chains).toEqual(['eip155:1', 'eip155:137'])
908
+ expect(caps.events).toEqual(['accountsChanged'])
909
+ })
910
+ })
911
+ })