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,71 +1,91 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import { WalletPairProvider } from './eip1193.js';
3
- import { DAppSession } from '../dapp-session.js';
4
- import { makeJoinBody, MockTransport } from '../test-helpers.js';
1
+ import { describe, expect, it, vi } from 'vitest'
2
+ import type { AadHeader } from '../crypto.js'
5
3
  import {
6
- generateX25519KeyPair,
4
+ b64urlDecode,
7
5
  computeSharedSecret,
8
6
  deriveSessionKey,
7
+ generateX25519KeyPair,
9
8
  sealPayload,
10
- b64urlDecode,
11
- } from '../crypto.js';
12
- import type { AadHeader } from '../crypto.js';
13
- import type { ProtocolMessage } from '../types.js';
9
+ } from '../crypto.js'
10
+ import { DAppSession } from '../dapp-session.js'
11
+ import { MockTransport, makeJoinBody } from '../test-helpers.js'
12
+ import type { ProtocolMessage } from '../types.js'
13
+ import { WalletPairProvider } from './eip1193.js'
14
14
 
15
15
  function flushMicrotasks(): Promise<void> {
16
- return new Promise((r) => setTimeout(r, 10));
16
+ return new Promise((r) => setTimeout(r, 10))
17
17
  }
18
18
 
19
19
  describe('WalletPairProvider', () => {
20
- let transport: MockTransport;
21
- let session: DAppSession;
22
- let provider: WalletPairProvider;
23
- let walletKp: ReturnType<typeof generateX25519KeyPair>;
24
- let sessionKey: Uint8Array;
20
+ let transport: MockTransport
21
+ let session: DAppSession
22
+ let provider: WalletPairProvider
23
+ let walletKp: ReturnType<typeof generateX25519KeyPair>
24
+ let sessionKey: Uint8Array
25
25
 
26
26
  async function setupConnectedSession(chainId = 1) {
27
- walletSendSeq = 0;
28
- transport = new MockTransport();
29
- session = new DAppSession({ transport, meta: { name: 'Test', description: 'Test dApp', url: 'https://test.com', icon: 'https://test.com/icon.png' } });
30
- provider = new WalletPairProvider({ session, chainId });
31
-
32
- await session.createPairing();
33
- walletKp = generateX25519KeyPair();
27
+ walletSendSeq = 0
28
+ transport = new MockTransport()
29
+ session = new DAppSession({
30
+ transport,
31
+ meta: {
32
+ name: 'Test',
33
+ description: 'Test dApp',
34
+ url: 'https://test.com',
35
+ icon: 'https://test.com/icon.png',
36
+ },
37
+ })
38
+ provider = new WalletPairProvider({ session, chainId })
39
+
40
+ await session.createPairing()
41
+ walletKp = generateX25519KeyPair()
34
42
 
35
43
  // Join
36
44
  transport.receive({
37
- v: 1, t: 'join', ch: session.channelId,
38
- ts: Date.now(), from: walletKp.publicKeyB64,
39
- body: makeJoinBody(session.channelId, transport.sent[0]!.from!, walletKp),
40
- } as ProtocolMessage);
45
+ v: 1,
46
+ t: 'join',
47
+ ch: session.channelId,
48
+ ts: Date.now(),
49
+ from: walletKp.publicKeyB64,
50
+ body: makeJoinBody(session.channelId, transport.sent[0]?.from!, walletKp),
51
+ } as ProtocolMessage)
41
52
 
42
53
  // Responses/events from the manual wallet use the wallet->dApp key.
43
- const dappPub = b64urlDecode(transport.sent[0]!.from!);
44
- const shared = computeSharedSecret(walletKp.privateKey, dappPub);
45
- deriveSessionKey(shared, session.channelId);
46
- sessionKey = (session as any).recvKey;
54
+ const dappPub = b64urlDecode(transport.sent[0]?.from!)
55
+ const shared = computeSharedSecret(walletKp.privateKey, dappPub)
56
+ deriveSessionKey(shared, session.channelId)
57
+ sessionKey = (session as any).recvKey
47
58
 
48
59
  // Connect
49
60
  transport.receive({
50
- v: 1, t: 'ready', ch: session.channelId,
51
- ts: Date.now(), from: '_adapter',
61
+ v: 1,
62
+ t: 'ready',
63
+ ch: session.channelId,
64
+ ts: Date.now(),
65
+ from: '_adapter',
52
66
  body: { state: 'connected', reconnect: false, remote: walletKp.publicKeyB64 },
53
- } as ProtocolMessage);
67
+ } as ProtocolMessage)
54
68
  }
55
69
 
56
- let walletSendSeq = 0;
70
+ let walletSendSeq = 0
57
71
 
58
72
  function respondToLatestReq(result: unknown, ok = true) {
59
- const reqMsg = [...transport.sent].reverse().find(m => m.t === 'req') as any;
60
- if (!reqMsg) throw new Error('No req found');
61
- const reqId = reqMsg.body.id;
62
- const hdr: AadHeader = { type: 'res', from: walletKp.publicKeyB64, id: reqId };
63
- const sealedData = ok ? { _ok: true, _result: result } : { _ok: false, ...result as object };
73
+ const reqMsg = [...transport.sent].reverse().find((m) => m.t === 'req') as any
74
+ if (!reqMsg) throw new Error('No req found')
75
+ const reqId = reqMsg.body.id
76
+ const hdr: AadHeader = { type: 'res', from: walletKp.publicKeyB64, id: reqId }
77
+ const sealedData = ok ? { _ok: true, _result: result } : { _ok: false, ...(result as object) }
64
78
  transport.receive({
65
- v: 1, t: 'res', ch: session.channelId,
66
- ts: Date.now(), from: walletKp.publicKeyB64,
67
- body: { id: reqId, sealed: sealPayload(sessionKey, session.channelId, walletSendSeq++, sealedData, hdr) },
68
- } as ProtocolMessage);
79
+ v: 1,
80
+ t: 'res',
81
+ ch: session.channelId,
82
+ ts: Date.now(),
83
+ from: walletKp.publicKeyB64,
84
+ body: {
85
+ id: reqId,
86
+ sealed: sealPayload(sessionKey, session.channelId, walletSendSeq++, sealedData, hdr),
87
+ },
88
+ } as ProtocolMessage)
69
89
  }
70
90
 
71
91
  // -----------------------------------------------------------------------
@@ -74,25 +94,25 @@ describe('WalletPairProvider', () => {
74
94
 
75
95
  describe('eth_chainId', () => {
76
96
  it('returns chain ID as hex (default mainnet)', async () => {
77
- await setupConnectedSession();
78
- const result = await provider.request({ method: 'eth_chainId' });
79
- expect(result).toBe('0x1');
80
- });
97
+ await setupConnectedSession()
98
+ const result = await provider.request({ method: 'eth_chainId' })
99
+ expect(result).toBe('0x1')
100
+ })
81
101
 
82
102
  it('returns custom chain ID', async () => {
83
- await setupConnectedSession(137);
84
- const result = await provider.request({ method: 'eth_chainId' });
85
- expect(result).toBe('0x89');
86
- });
87
- });
103
+ await setupConnectedSession(137)
104
+ const result = await provider.request({ method: 'eth_chainId' })
105
+ expect(result).toBe('0x89')
106
+ })
107
+ })
88
108
 
89
109
  describe('net_version', () => {
90
110
  it('returns chain ID as decimal string', async () => {
91
- await setupConnectedSession();
92
- const result = await provider.request({ method: 'net_version' });
93
- expect(result).toBe('1');
94
- });
95
- });
111
+ await setupConnectedSession()
112
+ const result = await provider.request({ method: 'net_version' })
113
+ expect(result).toBe('1')
114
+ })
115
+ })
96
116
 
97
117
  // -----------------------------------------------------------------------
98
118
  // eth_requestAccounts / eth_accounts
@@ -100,47 +120,47 @@ describe('WalletPairProvider', () => {
100
120
 
101
121
  describe('eth_requestAccounts', () => {
102
122
  it('maps to wallet_getAccounts and returns result', async () => {
103
- await setupConnectedSession();
123
+ await setupConnectedSession()
104
124
 
105
- const promise = provider.request({ method: 'eth_requestAccounts' });
106
- await flushMicrotasks();
125
+ const promise = provider.request({ method: 'eth_requestAccounts' })
126
+ await flushMicrotasks()
107
127
 
108
128
  // Verify it sent wallet_getAccounts
109
- const reqMsg = transport.sent.find(m => m.t === 'req') as any;
110
- expect(reqMsg.body.sealed).toBeTruthy(); // method inside sealed
129
+ const reqMsg = transport.sent.find((m) => m.t === 'req') as any
130
+ expect(reqMsg.body.sealed).toBeTruthy() // method inside sealed
111
131
 
112
- respondToLatestReq(['0xabc123']);
113
- const result = await promise;
114
- expect(result).toEqual(['0xabc123']);
115
- });
132
+ respondToLatestReq(['0xabc123'])
133
+ const result = await promise
134
+ expect(result).toEqual(['0xabc123'])
135
+ })
116
136
 
117
137
  it('caches accounts after request', async () => {
118
- await setupConnectedSession();
138
+ await setupConnectedSession()
119
139
 
120
- const promise = provider.request({ method: 'eth_requestAccounts' });
121
- await flushMicrotasks();
122
- respondToLatestReq(['0xabc123']);
123
- await promise;
140
+ const promise = provider.request({ method: 'eth_requestAccounts' })
141
+ await flushMicrotasks()
142
+ respondToLatestReq(['0xabc123'])
143
+ await promise
124
144
 
125
- expect(provider.getAccounts()).toEqual(['0xabc123']);
126
- });
127
- });
145
+ expect(provider.getAccounts()).toEqual(['0xabc123'])
146
+ })
147
+ })
128
148
 
129
149
  describe('eth_accounts', () => {
130
150
  it('maps to wallet_getAccounts', async () => {
131
- await setupConnectedSession();
151
+ await setupConnectedSession()
132
152
 
133
- const promise = provider.request({ method: 'eth_accounts' });
134
- await flushMicrotasks();
153
+ const promise = provider.request({ method: 'eth_accounts' })
154
+ await flushMicrotasks()
135
155
 
136
- const reqMsg = transport.sent.find(m => m.t === 'req') as any;
137
- expect(reqMsg.body.sealed).toBeTruthy(); // method inside sealed
156
+ const reqMsg = transport.sent.find((m) => m.t === 'req') as any
157
+ expect(reqMsg.body.sealed).toBeTruthy() // method inside sealed
138
158
 
139
- respondToLatestReq(['0x456']);
140
- const result = await promise;
141
- expect(result).toEqual(['0x456']);
142
- });
143
- });
159
+ respondToLatestReq(['0x456'])
160
+ const result = await promise
161
+ expect(result).toEqual(['0x456'])
162
+ })
163
+ })
144
164
 
145
165
  // -----------------------------------------------------------------------
146
166
  // personal_sign
@@ -148,41 +168,41 @@ describe('WalletPairProvider', () => {
148
168
 
149
169
  describe('personal_sign', () => {
150
170
  it('maps hex data to wallet_signMessage with decoded text', async () => {
151
- await setupConnectedSession();
171
+ await setupConnectedSession()
152
172
 
153
173
  // 0x48656c6c6f is "Hello" in hex
154
174
  const promise = provider.request({
155
175
  method: 'personal_sign',
156
176
  params: ['0x48656c6c6f', '0xabc'],
157
- });
158
- await flushMicrotasks();
177
+ })
178
+ await flushMicrotasks()
159
179
 
160
- const reqMsg = transport.sent.find(m => m.t === 'req') as any;
161
- expect(reqMsg.body.sealed).toBeTruthy(); // method inside sealed
180
+ const reqMsg = transport.sent.find((m) => m.t === 'req') as any
181
+ expect(reqMsg.body.sealed).toBeTruthy() // method inside sealed
162
182
 
163
183
  // Wallet responds with { signature }, mapResponse unwraps to just the string
164
- respondToLatestReq({ signature: '0xsig...' });
165
- const result = await promise;
166
- expect(result).toBe('0xsig...');
167
- });
184
+ respondToLatestReq({ signature: '0xsig...' })
185
+ const result = await promise
186
+ expect(result).toBe('0xsig...')
187
+ })
168
188
 
169
189
  it('maps plain text to wallet_signMessage', async () => {
170
- await setupConnectedSession();
190
+ await setupConnectedSession()
171
191
 
172
192
  const promise = provider.request({
173
193
  method: 'personal_sign',
174
194
  params: ['Hello, WalletPair!', '0xabc'],
175
- });
176
- await flushMicrotasks();
195
+ })
196
+ await flushMicrotasks()
177
197
 
178
- const reqMsg = transport.sent.find(m => m.t === 'req') as any;
179
- expect(reqMsg.body.sealed).toBeTruthy(); // method inside sealed
198
+ const reqMsg = transport.sent.find((m) => m.t === 'req') as any
199
+ expect(reqMsg.body.sealed).toBeTruthy() // method inside sealed
180
200
 
181
- respondToLatestReq({ signature: '0xsig...' });
182
- const result = await promise;
183
- expect(result).toBe('0xsig...');
184
- });
185
- });
201
+ respondToLatestReq({ signature: '0xsig...' })
202
+ const result = await promise
203
+ expect(result).toBe('0xsig...')
204
+ })
205
+ })
186
206
 
187
207
  // -----------------------------------------------------------------------
188
208
  // eth_sendTransaction
@@ -190,24 +210,24 @@ describe('WalletPairProvider', () => {
190
210
 
191
211
  describe('eth_sendTransaction', () => {
192
212
  it('maps to wallet_sendTransaction', async () => {
193
- await setupConnectedSession();
213
+ await setupConnectedSession()
194
214
 
195
- const tx = { to: '0x123', value: '0x0', data: '0x' };
215
+ const tx = { to: '0x123', value: '0x0', data: '0x', type: '0x2', chainId: '0x1' }
196
216
  const promise = provider.request({
197
217
  method: 'eth_sendTransaction',
198
218
  params: [tx],
199
- });
200
- await flushMicrotasks();
219
+ })
220
+ await flushMicrotasks()
201
221
 
202
- const reqMsg = transport.sent.find(m => m.t === 'req') as any;
203
- expect(reqMsg.body.sealed).toBeTruthy(); // method inside sealed
222
+ const reqMsg = transport.sent.find((m) => m.t === 'req') as any
223
+ expect(reqMsg.body.sealed).toBeTruthy() // method inside sealed
204
224
 
205
225
  // mapResponse unwraps { txHash } to just the hash string
206
- respondToLatestReq({ txHash: '0xtx...' });
207
- const result = await promise;
208
- expect(result).toBe('0xtx...');
209
- });
210
- });
226
+ respondToLatestReq({ txHash: '0xtx...' })
227
+ const result = await promise
228
+ expect(result).toBe('0xtx...')
229
+ })
230
+ })
211
231
 
212
232
  // -----------------------------------------------------------------------
213
233
  // wallet_switchEthereumChain
@@ -215,21 +235,21 @@ describe('WalletPairProvider', () => {
215
235
 
216
236
  describe('wallet_switchEthereumChain', () => {
217
237
  it('maps to wallet_switchChain', async () => {
218
- await setupConnectedSession();
238
+ await setupConnectedSession()
219
239
 
220
240
  const promise = provider.request({
221
241
  method: 'wallet_switchEthereumChain',
222
242
  params: [{ chainId: '0x89' }],
223
- });
224
- await flushMicrotasks();
243
+ })
244
+ await flushMicrotasks()
225
245
 
226
- const reqMsg = transport.sent.find(m => m.t === 'req') as any;
227
- expect(reqMsg.body.sealed).toBeTruthy(); // method inside sealed
246
+ const reqMsg = transport.sent.find((m) => m.t === 'req') as any
247
+ expect(reqMsg.body.sealed).toBeTruthy() // method inside sealed
228
248
 
229
- respondToLatestReq({ success: true });
230
- await promise;
231
- });
232
- });
249
+ respondToLatestReq({ success: true })
250
+ await promise
251
+ })
252
+ })
233
253
 
234
254
  // -----------------------------------------------------------------------
235
255
  // Events
@@ -237,79 +257,127 @@ describe('WalletPairProvider', () => {
237
257
 
238
258
  describe('EIP-1193 events', () => {
239
259
  it('emits connect when session connects', async () => {
240
- await setupConnectedSession();
260
+ await setupConnectedSession()
241
261
  // Provider constructor registered the listener, and session is already connected
242
- expect(provider.isConnected()).toBe(true);
243
- });
262
+ expect(provider.isConnected()).toBe(true)
263
+ })
244
264
 
245
265
  it('emits disconnect when session closes', async () => {
246
- await setupConnectedSession();
247
- const handler = vi.fn();
248
- provider.on('disconnect', handler);
266
+ await setupConnectedSession()
267
+ const handler = vi.fn()
268
+ provider.on('disconnect', handler)
249
269
 
250
- session.close();
251
- expect(handler).toHaveBeenCalledWith({ code: 4900, message: 'Disconnected' });
252
- expect(provider.isConnected()).toBe(false);
253
- });
270
+ session.close()
271
+ expect(handler).toHaveBeenCalledWith({ code: 4900, message: 'Disconnected' })
272
+ expect(provider.isConnected()).toBe(false)
273
+ })
254
274
 
255
275
  it('emits accountsChanged from wallet event', async () => {
256
- await setupConnectedSession();
257
- const handler = vi.fn();
258
- provider.on('accountsChanged', handler);
276
+ await setupConnectedSession()
277
+ const handler = vi.fn()
278
+ provider.on('accountsChanged', handler)
259
279
 
260
280
  transport.receive({
261
- v: 1, t: 'evt', ch: session.channelId,
262
- ts: Date.now(), from: walletKp.publicKeyB64,
263
- body: { id: 'evt-1', sealed: sealPayload(sessionKey, session.channelId, 0, { _event: 'accountsChanged', accounts: ['0xnew'] }, { type: 'evt', from: walletKp.publicKeyB64, id: 'evt-1' }) },
264
- } as ProtocolMessage);
265
-
266
- expect(handler).toHaveBeenCalledWith(['0xnew']);
267
- expect(provider.getAccounts()).toEqual(['0xnew']);
268
- });
281
+ v: 1,
282
+ t: 'evt',
283
+ ch: session.channelId,
284
+ ts: Date.now(),
285
+ from: walletKp.publicKeyB64,
286
+ body: {
287
+ id: 'evt-1',
288
+ sealed: sealPayload(
289
+ sessionKey,
290
+ session.channelId,
291
+ 0,
292
+ { _event: 'accountsChanged', accounts: ['0xnew'] },
293
+ { type: 'evt', from: walletKp.publicKeyB64, id: 'evt-1' },
294
+ ),
295
+ },
296
+ } as ProtocolMessage)
297
+
298
+ expect(handler).toHaveBeenCalledWith(['0xnew'])
299
+ expect(provider.getAccounts()).toEqual(['0xnew'])
300
+ })
269
301
 
270
302
  it('emits chainChanged from wallet event (CAIP-2 format)', async () => {
271
- await setupConnectedSession();
272
- const handler = vi.fn();
273
- provider.on('chainChanged', handler);
303
+ await setupConnectedSession()
304
+ const handler = vi.fn()
305
+ provider.on('chainChanged', handler)
274
306
 
275
307
  transport.receive({
276
- v: 1, t: 'evt', ch: session.channelId,
277
- ts: Date.now(), from: walletKp.publicKeyB64,
278
- body: { id: 'evt-2', sealed: sealPayload(sessionKey, session.channelId, 0, { _event: 'chainChanged', chainId: 'eip155:137' }, { type: 'evt', from: walletKp.publicKeyB64, id: 'evt-2' }) },
279
- } as ProtocolMessage);
280
-
281
- expect(handler).toHaveBeenCalledWith('0x89');
282
- });
308
+ v: 1,
309
+ t: 'evt',
310
+ ch: session.channelId,
311
+ ts: Date.now(),
312
+ from: walletKp.publicKeyB64,
313
+ body: {
314
+ id: 'evt-2',
315
+ sealed: sealPayload(
316
+ sessionKey,
317
+ session.channelId,
318
+ 0,
319
+ { _event: 'chainChanged', chainId: 'eip155:137' },
320
+ { type: 'evt', from: walletKp.publicKeyB64, id: 'evt-2' },
321
+ ),
322
+ },
323
+ } as ProtocolMessage)
324
+
325
+ expect(handler).toHaveBeenCalledWith('0x89')
326
+ })
283
327
 
284
328
  it('emits chainChanged from wallet event (hex format)', async () => {
285
- await setupConnectedSession();
286
- const handler = vi.fn();
287
- provider.on('chainChanged', handler);
329
+ await setupConnectedSession()
330
+ const handler = vi.fn()
331
+ provider.on('chainChanged', handler)
288
332
 
289
333
  transport.receive({
290
- v: 1, t: 'evt', ch: session.channelId,
291
- ts: Date.now(), from: walletKp.publicKeyB64,
292
- body: { id: 'evt-3', sealed: sealPayload(sessionKey, session.channelId, 0, { _event: 'chainChanged', chainId: '0x89' }, { type: 'evt', from: walletKp.publicKeyB64, id: 'evt-3' }) },
293
- } as ProtocolMessage);
294
-
295
- expect(handler).toHaveBeenCalledWith('0x89');
296
- });
334
+ v: 1,
335
+ t: 'evt',
336
+ ch: session.channelId,
337
+ ts: Date.now(),
338
+ from: walletKp.publicKeyB64,
339
+ body: {
340
+ id: 'evt-3',
341
+ sealed: sealPayload(
342
+ sessionKey,
343
+ session.channelId,
344
+ 0,
345
+ { _event: 'chainChanged', chainId: '0x89' },
346
+ { type: 'evt', from: walletKp.publicKeyB64, id: 'evt-3' },
347
+ ),
348
+ },
349
+ } as ProtocolMessage)
350
+
351
+ expect(handler).toHaveBeenCalledWith('0x89')
352
+ })
297
353
 
298
354
  it('removeListener stops event delivery', async () => {
299
- await setupConnectedSession();
300
- const handler = vi.fn();
301
- provider.on('accountsChanged', handler);
302
- provider.removeListener('accountsChanged', handler);
355
+ await setupConnectedSession()
356
+ const handler = vi.fn()
357
+ provider.on('accountsChanged', handler)
358
+ provider.removeListener('accountsChanged', handler)
303
359
 
304
360
  transport.receive({
305
- v: 1, t: 'evt', ch: session.channelId,
306
- ts: Date.now(), from: walletKp.publicKeyB64,
307
- body: { id: 'evt-4', sealed: sealPayload(sessionKey, session.channelId, 0, { _event: 'accountsChanged', accounts: ['0x1'] }, { type: 'evt', from: walletKp.publicKeyB64, id: 'evt-4' }) },
308
- } as ProtocolMessage);
309
-
310
- expect(handler).not.toHaveBeenCalled();
311
- });
312
- });
361
+ v: 1,
362
+ t: 'evt',
363
+ ch: session.channelId,
364
+ ts: Date.now(),
365
+ from: walletKp.publicKeyB64,
366
+ body: {
367
+ id: 'evt-4',
368
+ sealed: sealPayload(
369
+ sessionKey,
370
+ session.channelId,
371
+ 0,
372
+ { _event: 'accountsChanged', accounts: ['0x1'] },
373
+ { type: 'evt', from: walletKp.publicKeyB64, id: 'evt-4' },
374
+ ),
375
+ },
376
+ } as ProtocolMessage)
377
+
378
+ expect(handler).not.toHaveBeenCalled()
379
+ })
380
+ })
313
381
 
314
382
  // -----------------------------------------------------------------------
315
383
  // Helper methods
@@ -317,15 +385,15 @@ describe('WalletPairProvider', () => {
317
385
 
318
386
  describe('helper methods', () => {
319
387
  it('getChainId returns hex string', async () => {
320
- await setupConnectedSession(42161);
321
- expect(provider.getChainId()).toBe('0xa4b1');
322
- });
388
+ await setupConnectedSession(42161)
389
+ expect(provider.getChainId()).toBe('0xa4b1')
390
+ })
323
391
 
324
392
  it('getSession returns the underlying session', async () => {
325
- await setupConnectedSession();
326
- expect(provider.getSession()).toBe(session);
327
- });
328
- });
393
+ await setupConnectedSession()
394
+ expect(provider.getSession()).toBe(session)
395
+ })
396
+ })
329
397
 
330
398
  // -----------------------------------------------------------------------
331
399
  // Error handling
@@ -333,33 +401,43 @@ describe('WalletPairProvider', () => {
333
401
 
334
402
  describe('error handling', () => {
335
403
  it('rejects on wallet error response', async () => {
336
- await setupConnectedSession();
404
+ await setupConnectedSession()
337
405
 
338
- const promise = provider.request({ method: 'eth_requestAccounts' });
339
- await flushMicrotasks();
406
+ const promise = provider.request({ method: 'eth_requestAccounts' })
407
+ await flushMicrotasks()
340
408
 
341
- respondToLatestReq({ code: 'user_rejected', message: 'Denied' }, false);
342
- await expect(promise).rejects.toThrow('Denied');
343
- });
344
- });
409
+ respondToLatestReq({ code: 'user_rejected', message: 'Denied' }, false)
410
+ await expect(promise).rejects.toThrow('Denied')
411
+ })
412
+ })
345
413
 
346
414
  // -----------------------------------------------------------------------
347
415
  // Passthrough methods
348
416
  // -----------------------------------------------------------------------
349
417
 
350
418
  describe('unknown methods', () => {
351
- it('passes unknown methods through to wallet', async () => {
352
- await setupConnectedSession();
353
-
354
- const promise = provider.request({ method: 'eth_getBalance', params: ['0x123', 'latest'] });
355
- await flushMicrotasks();
356
-
357
- const reqMsg = transport.sent.find(m => m.t === 'req') as any;
358
- expect(reqMsg.body.sealed).toBeTruthy(); // method inside sealed
359
-
360
- respondToLatestReq('0x1234');
361
- const result = await promise;
362
- expect(result).toBe('0x1234');
363
- });
364
- });
365
- });
419
+ it('routes read-only methods to rpcProvider when available', async () => {
420
+ await setupConnectedSession()
421
+ const rpcProvider = { request: vi.fn().mockResolvedValue('0x1234') }
422
+ ;(provider as any).rpcProvider = rpcProvider
423
+
424
+ const result = await provider.request({
425
+ method: 'eth_getBalance',
426
+ params: ['0x123', 'latest'],
427
+ })
428
+ expect(rpcProvider.request).toHaveBeenCalledWith({
429
+ method: 'eth_getBalance',
430
+ params: ['0x123', 'latest'],
431
+ })
432
+ expect(result).toBe('0x1234')
433
+ })
434
+
435
+ it('throws for read-only methods without rpcProvider', async () => {
436
+ await setupConnectedSession()
437
+
438
+ await expect(
439
+ provider.request({ method: 'eth_getBalance', params: ['0x123', 'latest'] }),
440
+ ).rejects.toThrow('Unsupported method: eth_getBalance')
441
+ })
442
+ })
443
+ })