walletpair-sdk 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +415 -0
  3. package/dist/ble/framing.d.ts +23 -0
  4. package/dist/ble/framing.d.ts.map +1 -0
  5. package/dist/ble/framing.js +83 -0
  6. package/dist/ble/framing.js.map +1 -0
  7. package/dist/ble/index.d.ts +9 -0
  8. package/dist/ble/index.d.ts.map +1 -0
  9. package/dist/ble/index.js +9 -0
  10. package/dist/ble/index.js.map +1 -0
  11. package/dist/ble/web-ble-transport.d.ts +29 -0
  12. package/dist/ble/web-ble-transport.d.ts.map +1 -0
  13. package/dist/ble/web-ble-transport.js +93 -0
  14. package/dist/ble/web-ble-transport.js.map +1 -0
  15. package/dist/crypto.d.ts +102 -0
  16. package/dist/crypto.d.ts.map +1 -0
  17. package/dist/crypto.js +279 -0
  18. package/dist/crypto.js.map +1 -0
  19. package/dist/dapp-session.d.ts +106 -0
  20. package/dist/dapp-session.d.ts.map +1 -0
  21. package/dist/dapp-session.js +918 -0
  22. package/dist/dapp-session.js.map +1 -0
  23. package/dist/emitter.d.ts +16 -0
  24. package/dist/emitter.d.ts.map +1 -0
  25. package/dist/emitter.js +41 -0
  26. package/dist/emitter.js.map +1 -0
  27. package/dist/evm/eip1193.d.ts +83 -0
  28. package/dist/evm/eip1193.d.ts.map +1 -0
  29. package/dist/evm/eip1193.js +270 -0
  30. package/dist/evm/eip1193.js.map +1 -0
  31. package/dist/evm/index.d.ts +8 -0
  32. package/dist/evm/index.d.ts.map +1 -0
  33. package/dist/evm/index.js +8 -0
  34. package/dist/evm/index.js.map +1 -0
  35. package/dist/evm/wagmi.d.ts +118 -0
  36. package/dist/evm/wagmi.d.ts.map +1 -0
  37. package/dist/evm/wagmi.js +205 -0
  38. package/dist/evm/wagmi.js.map +1 -0
  39. package/dist/index.d.ts +22 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +24 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/types.d.ts +225 -0
  44. package/dist/types.d.ts.map +1 -0
  45. package/dist/types.js +31 -0
  46. package/dist/types.js.map +1 -0
  47. package/dist/wallet-session.d.ts +107 -0
  48. package/dist/wallet-session.d.ts.map +1 -0
  49. package/dist/wallet-session.js +794 -0
  50. package/dist/wallet-session.js.map +1 -0
  51. package/dist/ws-transport.d.ts +29 -0
  52. package/dist/ws-transport.d.ts.map +1 -0
  53. package/dist/ws-transport.js +79 -0
  54. package/dist/ws-transport.js.map +1 -0
  55. package/package.json +55 -0
  56. package/src/__tests__/adversarial/crypto-attacks.test.ts +557 -0
  57. package/src/__tests__/adversarial/malicious-dapp.test.ts +505 -0
  58. package/src/__tests__/adversarial/malicious-relay.test.ts +528 -0
  59. package/src/__tests__/adversarial/malicious-wallet.test.ts +467 -0
  60. package/src/__tests__/spec-compliance/canonical-json.test.ts +227 -0
  61. package/src/__tests__/spec-compliance/crypto-vectors.test.ts +321 -0
  62. package/src/__tests__/spec-compliance/message-format.test.ts +356 -0
  63. package/src/__tests__/spec-compliance/sequence-numbers.test.ts +300 -0
  64. package/src/__tests__/spec-compliance/state-machine.test.ts +364 -0
  65. package/src/ble/framing.test.ts +196 -0
  66. package/src/ble/framing.ts +100 -0
  67. package/src/ble/index.ts +18 -0
  68. package/src/ble/web-ble-transport.test.ts +192 -0
  69. package/src/ble/web-ble-transport.ts +116 -0
  70. package/src/ble/web-bluetooth.d.ts +47 -0
  71. package/src/canonical-json.test.ts +612 -0
  72. package/src/crypto-directional.test.ts +263 -0
  73. package/src/crypto-hardening.test.ts +529 -0
  74. package/src/crypto.test.ts +635 -0
  75. package/src/crypto.ts +405 -0
  76. package/src/dapp-session.test.ts +647 -0
  77. package/src/dapp-session.ts +1004 -0
  78. package/src/emitter.test.ts +169 -0
  79. package/src/emitter.ts +45 -0
  80. package/src/evm/eip1193.test.ts +365 -0
  81. package/src/evm/eip1193.ts +346 -0
  82. package/src/evm/index.ts +19 -0
  83. package/src/evm/wagmi.test.ts +396 -0
  84. package/src/evm/wagmi.ts +321 -0
  85. package/src/index.ts +86 -0
  86. package/src/integration.test.ts +385 -0
  87. package/src/security.test.ts +430 -0
  88. package/src/sequence-validation.test.ts +1185 -0
  89. package/src/test-helpers.ts +216 -0
  90. package/src/types.test.ts +82 -0
  91. package/src/types.ts +305 -0
  92. package/src/wallet-session.test.ts +683 -0
  93. package/src/wallet-session.ts +922 -0
  94. package/src/ws-transport.test.ts +231 -0
  95. package/src/ws-transport.ts +92 -0
@@ -0,0 +1,169 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { Emitter } from './emitter.js';
3
+
4
+ interface TestEvents {
5
+ [key: string]: unknown;
6
+ message: string;
7
+ count: number;
8
+ complex: { name: string; value: number };
9
+ }
10
+
11
+ describe('Emitter', () => {
12
+ it('emits events to registered handlers', () => {
13
+ const emitter = new Emitter<TestEvents>();
14
+ const handler = vi.fn();
15
+ emitter.on('message', handler);
16
+ emitter.emit('message', 'hello');
17
+ expect(handler).toHaveBeenCalledWith('hello');
18
+ expect(handler).toHaveBeenCalledTimes(1);
19
+ });
20
+
21
+ it('supports multiple handlers for the same event', () => {
22
+ const emitter = new Emitter<TestEvents>();
23
+ const h1 = vi.fn();
24
+ const h2 = vi.fn();
25
+ emitter.on('message', h1);
26
+ emitter.on('message', h2);
27
+ emitter.emit('message', 'test');
28
+ expect(h1).toHaveBeenCalledWith('test');
29
+ expect(h2).toHaveBeenCalledWith('test');
30
+ });
31
+
32
+ it('supports multiple event types', () => {
33
+ const emitter = new Emitter<TestEvents>();
34
+ const msgHandler = vi.fn();
35
+ const countHandler = vi.fn();
36
+ emitter.on('message', msgHandler);
37
+ emitter.on('count', countHandler);
38
+
39
+ emitter.emit('message', 'hello');
40
+ emitter.emit('count', 42);
41
+
42
+ expect(msgHandler).toHaveBeenCalledWith('hello');
43
+ expect(countHandler).toHaveBeenCalledWith(42);
44
+ expect(msgHandler).toHaveBeenCalledTimes(1);
45
+ expect(countHandler).toHaveBeenCalledTimes(1);
46
+ });
47
+
48
+ it('on() returns an unsubscribe function', () => {
49
+ const emitter = new Emitter<TestEvents>();
50
+ const handler = vi.fn();
51
+ const off = emitter.on('message', handler);
52
+
53
+ emitter.emit('message', 'first');
54
+ expect(handler).toHaveBeenCalledTimes(1);
55
+
56
+ off();
57
+ emitter.emit('message', 'second');
58
+ expect(handler).toHaveBeenCalledTimes(1); // not called again
59
+ });
60
+
61
+ it('off() removes a specific handler', () => {
62
+ const emitter = new Emitter<TestEvents>();
63
+ const h1 = vi.fn();
64
+ const h2 = vi.fn();
65
+ emitter.on('message', h1);
66
+ emitter.on('message', h2);
67
+
68
+ emitter.off('message', h1);
69
+ emitter.emit('message', 'test');
70
+
71
+ expect(h1).not.toHaveBeenCalled();
72
+ expect(h2).toHaveBeenCalledWith('test');
73
+ });
74
+
75
+ it('off() without handler removes all handlers for that event', () => {
76
+ const emitter = new Emitter<TestEvents>();
77
+ const h1 = vi.fn();
78
+ const h2 = vi.fn();
79
+ emitter.on('message', h1);
80
+ emitter.on('message', h2);
81
+
82
+ emitter.off('message');
83
+ emitter.emit('message', 'test');
84
+
85
+ expect(h1).not.toHaveBeenCalled();
86
+ expect(h2).not.toHaveBeenCalled();
87
+ });
88
+
89
+ it('once() fires handler only once', () => {
90
+ const emitter = new Emitter<TestEvents>();
91
+ const handler = vi.fn();
92
+ emitter.once('message', handler);
93
+
94
+ emitter.emit('message', 'first');
95
+ emitter.emit('message', 'second');
96
+
97
+ expect(handler).toHaveBeenCalledTimes(1);
98
+ expect(handler).toHaveBeenCalledWith('first');
99
+ });
100
+
101
+ it('once() returns an unsubscribe function that works before emit', () => {
102
+ const emitter = new Emitter<TestEvents>();
103
+ const handler = vi.fn();
104
+ const off = emitter.once('message', handler);
105
+
106
+ off(); // cancel before any emission
107
+ emitter.emit('message', 'test');
108
+ expect(handler).not.toHaveBeenCalled();
109
+ });
110
+
111
+ it('removeAll() clears all events', () => {
112
+ const emitter = new Emitter<TestEvents>();
113
+ const h1 = vi.fn();
114
+ const h2 = vi.fn();
115
+ emitter.on('message', h1);
116
+ emitter.on('count', h2);
117
+
118
+ emitter.removeAll();
119
+ emitter.emit('message', 'test');
120
+ emitter.emit('count', 1);
121
+
122
+ expect(h1).not.toHaveBeenCalled();
123
+ expect(h2).not.toHaveBeenCalled();
124
+ });
125
+
126
+ it('emitting with no handlers does not throw', () => {
127
+ const emitter = new Emitter<TestEvents>();
128
+ expect(() => emitter.emit('message', 'test')).not.toThrow();
129
+ });
130
+
131
+ it('handles complex event data', () => {
132
+ const emitter = new Emitter<TestEvents>();
133
+ const handler = vi.fn();
134
+ emitter.on('complex', handler);
135
+
136
+ const data = { name: 'test', value: 99 };
137
+ emitter.emit('complex', data);
138
+ expect(handler).toHaveBeenCalledWith(data);
139
+ });
140
+
141
+ it('handler added during emit is not called in the same emit cycle', () => {
142
+ const emitter = new Emitter<TestEvents>();
143
+ const late = vi.fn();
144
+ emitter.on('message', () => {
145
+ emitter.on('message', late);
146
+ });
147
+ emitter.emit('message', 'trigger');
148
+ // The late handler was added during iteration, behavior depends on Set iteration
149
+ // but it should not cause errors
150
+ });
151
+
152
+ it('multiple on() calls return independent unsubscribe functions', () => {
153
+ const emitter = new Emitter<TestEvents>();
154
+ const handler = vi.fn();
155
+ const off1 = emitter.on('message', handler);
156
+ const off2 = emitter.on('count', handler);
157
+
158
+ off1();
159
+ emitter.emit('message', 'gone');
160
+ emitter.emit('count', 42);
161
+
162
+ expect(handler).toHaveBeenCalledTimes(1);
163
+ expect(handler).toHaveBeenCalledWith(42);
164
+
165
+ off2();
166
+ emitter.emit('count', 99);
167
+ expect(handler).toHaveBeenCalledTimes(1);
168
+ });
169
+ });
package/src/emitter.ts ADDED
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Minimal typed event emitter — no external dependencies.
3
+ */
4
+
5
+ type Handler<T> = (data: T) => void;
6
+
7
+ // biome-ignore lint: index signature needed for generic emitter
8
+ export class Emitter<Events extends { [key: string]: unknown }> {
9
+ private handlers = new Map<keyof Events, Set<Handler<any>>>();
10
+
11
+ on<K extends keyof Events>(event: K, handler: Handler<Events[K]>): () => void {
12
+ let set = this.handlers.get(event);
13
+ if (!set) {
14
+ set = new Set();
15
+ this.handlers.set(event, set);
16
+ }
17
+ set.add(handler);
18
+ return () => { set!.delete(handler); };
19
+ }
20
+
21
+ once<K extends keyof Events>(event: K, handler: Handler<Events[K]>): () => void {
22
+ const off = this.on(event, (data) => {
23
+ off();
24
+ handler(data);
25
+ });
26
+ return off;
27
+ }
28
+
29
+ emit<K extends keyof Events>(event: K, data: Events[K]): void {
30
+ const set = this.handlers.get(event);
31
+ if (set) for (const h of set) h(data);
32
+ }
33
+
34
+ off<K extends keyof Events>(event: K, handler?: Handler<Events[K]>): void {
35
+ if (handler) {
36
+ this.handlers.get(event)?.delete(handler);
37
+ } else {
38
+ this.handlers.delete(event);
39
+ }
40
+ }
41
+
42
+ removeAll(): void {
43
+ this.handlers.clear();
44
+ }
45
+ }
@@ -0,0 +1,365 @@
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';
5
+ import {
6
+ generateX25519KeyPair,
7
+ computeSharedSecret,
8
+ deriveSessionKey,
9
+ sealPayload,
10
+ b64urlDecode,
11
+ } from '../crypto.js';
12
+ import type { AadHeader } from '../crypto.js';
13
+ import type { ProtocolMessage } from '../types.js';
14
+
15
+ function flushMicrotasks(): Promise<void> {
16
+ return new Promise((r) => setTimeout(r, 10));
17
+ }
18
+
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;
25
+
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();
34
+
35
+ // Join
36
+ 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);
41
+
42
+ // 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;
47
+
48
+ // Connect
49
+ transport.receive({
50
+ v: 1, t: 'ready', ch: session.channelId,
51
+ ts: Date.now(), from: '_adapter',
52
+ body: { state: 'connected', reconnect: false, remote: walletKp.publicKeyB64 },
53
+ } as ProtocolMessage);
54
+ }
55
+
56
+ let walletSendSeq = 0;
57
+
58
+ 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 };
64
+ 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);
69
+ }
70
+
71
+ // -----------------------------------------------------------------------
72
+ // eth_chainId
73
+ // -----------------------------------------------------------------------
74
+
75
+ describe('eth_chainId', () => {
76
+ 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
+ });
81
+
82
+ 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
+ });
88
+
89
+ describe('net_version', () => {
90
+ 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
+ });
96
+
97
+ // -----------------------------------------------------------------------
98
+ // eth_requestAccounts / eth_accounts
99
+ // -----------------------------------------------------------------------
100
+
101
+ describe('eth_requestAccounts', () => {
102
+ it('maps to wallet_getAccounts and returns result', async () => {
103
+ await setupConnectedSession();
104
+
105
+ const promise = provider.request({ method: 'eth_requestAccounts' });
106
+ await flushMicrotasks();
107
+
108
+ // 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
111
+
112
+ respondToLatestReq(['0xabc123']);
113
+ const result = await promise;
114
+ expect(result).toEqual(['0xabc123']);
115
+ });
116
+
117
+ it('caches accounts after request', async () => {
118
+ await setupConnectedSession();
119
+
120
+ const promise = provider.request({ method: 'eth_requestAccounts' });
121
+ await flushMicrotasks();
122
+ respondToLatestReq(['0xabc123']);
123
+ await promise;
124
+
125
+ expect(provider.getAccounts()).toEqual(['0xabc123']);
126
+ });
127
+ });
128
+
129
+ describe('eth_accounts', () => {
130
+ it('maps to wallet_getAccounts', async () => {
131
+ await setupConnectedSession();
132
+
133
+ const promise = provider.request({ method: 'eth_accounts' });
134
+ await flushMicrotasks();
135
+
136
+ const reqMsg = transport.sent.find(m => m.t === 'req') as any;
137
+ expect(reqMsg.body.sealed).toBeTruthy(); // method inside sealed
138
+
139
+ respondToLatestReq(['0x456']);
140
+ const result = await promise;
141
+ expect(result).toEqual(['0x456']);
142
+ });
143
+ });
144
+
145
+ // -----------------------------------------------------------------------
146
+ // personal_sign
147
+ // -----------------------------------------------------------------------
148
+
149
+ describe('personal_sign', () => {
150
+ it('maps hex data to wallet_signMessage with decoded text', async () => {
151
+ await setupConnectedSession();
152
+
153
+ // 0x48656c6c6f is "Hello" in hex
154
+ const promise = provider.request({
155
+ method: 'personal_sign',
156
+ params: ['0x48656c6c6f', '0xabc'],
157
+ });
158
+ await flushMicrotasks();
159
+
160
+ const reqMsg = transport.sent.find(m => m.t === 'req') as any;
161
+ expect(reqMsg.body.sealed).toBeTruthy(); // method inside sealed
162
+
163
+ // 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
+ });
168
+
169
+ it('maps plain text to wallet_signMessage', async () => {
170
+ await setupConnectedSession();
171
+
172
+ const promise = provider.request({
173
+ method: 'personal_sign',
174
+ params: ['Hello, WalletPair!', '0xabc'],
175
+ });
176
+ await flushMicrotasks();
177
+
178
+ const reqMsg = transport.sent.find(m => m.t === 'req') as any;
179
+ expect(reqMsg.body.sealed).toBeTruthy(); // method inside sealed
180
+
181
+ respondToLatestReq({ signature: '0xsig...' });
182
+ const result = await promise;
183
+ expect(result).toBe('0xsig...');
184
+ });
185
+ });
186
+
187
+ // -----------------------------------------------------------------------
188
+ // eth_sendTransaction
189
+ // -----------------------------------------------------------------------
190
+
191
+ describe('eth_sendTransaction', () => {
192
+ it('maps to wallet_sendTransaction', async () => {
193
+ await setupConnectedSession();
194
+
195
+ const tx = { to: '0x123', value: '0x0', data: '0x' };
196
+ const promise = provider.request({
197
+ method: 'eth_sendTransaction',
198
+ params: [tx],
199
+ });
200
+ await flushMicrotasks();
201
+
202
+ const reqMsg = transport.sent.find(m => m.t === 'req') as any;
203
+ expect(reqMsg.body.sealed).toBeTruthy(); // method inside sealed
204
+
205
+ // mapResponse unwraps { txHash } to just the hash string
206
+ respondToLatestReq({ txHash: '0xtx...' });
207
+ const result = await promise;
208
+ expect(result).toBe('0xtx...');
209
+ });
210
+ });
211
+
212
+ // -----------------------------------------------------------------------
213
+ // wallet_switchEthereumChain
214
+ // -----------------------------------------------------------------------
215
+
216
+ describe('wallet_switchEthereumChain', () => {
217
+ it('maps to wallet_switchChain', async () => {
218
+ await setupConnectedSession();
219
+
220
+ const promise = provider.request({
221
+ method: 'wallet_switchEthereumChain',
222
+ params: [{ chainId: '0x89' }],
223
+ });
224
+ await flushMicrotasks();
225
+
226
+ const reqMsg = transport.sent.find(m => m.t === 'req') as any;
227
+ expect(reqMsg.body.sealed).toBeTruthy(); // method inside sealed
228
+
229
+ respondToLatestReq({ success: true });
230
+ await promise;
231
+ });
232
+ });
233
+
234
+ // -----------------------------------------------------------------------
235
+ // Events
236
+ // -----------------------------------------------------------------------
237
+
238
+ describe('EIP-1193 events', () => {
239
+ it('emits connect when session connects', async () => {
240
+ await setupConnectedSession();
241
+ // Provider constructor registered the listener, and session is already connected
242
+ expect(provider.isConnected()).toBe(true);
243
+ });
244
+
245
+ it('emits disconnect when session closes', async () => {
246
+ await setupConnectedSession();
247
+ const handler = vi.fn();
248
+ provider.on('disconnect', handler);
249
+
250
+ session.close();
251
+ expect(handler).toHaveBeenCalledWith({ code: 4900, message: 'Disconnected' });
252
+ expect(provider.isConnected()).toBe(false);
253
+ });
254
+
255
+ it('emits accountsChanged from wallet event', async () => {
256
+ await setupConnectedSession();
257
+ const handler = vi.fn();
258
+ provider.on('accountsChanged', handler);
259
+
260
+ 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
+ });
269
+
270
+ it('emits chainChanged from wallet event (CAIP-2 format)', async () => {
271
+ await setupConnectedSession();
272
+ const handler = vi.fn();
273
+ provider.on('chainChanged', handler);
274
+
275
+ 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
+ });
283
+
284
+ it('emits chainChanged from wallet event (hex format)', async () => {
285
+ await setupConnectedSession();
286
+ const handler = vi.fn();
287
+ provider.on('chainChanged', handler);
288
+
289
+ 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
+ });
297
+
298
+ it('removeListener stops event delivery', async () => {
299
+ await setupConnectedSession();
300
+ const handler = vi.fn();
301
+ provider.on('accountsChanged', handler);
302
+ provider.removeListener('accountsChanged', handler);
303
+
304
+ 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
+ });
313
+
314
+ // -----------------------------------------------------------------------
315
+ // Helper methods
316
+ // -----------------------------------------------------------------------
317
+
318
+ describe('helper methods', () => {
319
+ it('getChainId returns hex string', async () => {
320
+ await setupConnectedSession(42161);
321
+ expect(provider.getChainId()).toBe('0xa4b1');
322
+ });
323
+
324
+ it('getSession returns the underlying session', async () => {
325
+ await setupConnectedSession();
326
+ expect(provider.getSession()).toBe(session);
327
+ });
328
+ });
329
+
330
+ // -----------------------------------------------------------------------
331
+ // Error handling
332
+ // -----------------------------------------------------------------------
333
+
334
+ describe('error handling', () => {
335
+ it('rejects on wallet error response', async () => {
336
+ await setupConnectedSession();
337
+
338
+ const promise = provider.request({ method: 'eth_requestAccounts' });
339
+ await flushMicrotasks();
340
+
341
+ respondToLatestReq({ code: 'user_rejected', message: 'Denied' }, false);
342
+ await expect(promise).rejects.toThrow('Denied');
343
+ });
344
+ });
345
+
346
+ // -----------------------------------------------------------------------
347
+ // Passthrough methods
348
+ // -----------------------------------------------------------------------
349
+
350
+ 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
+ });