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,385 @@
1
+ /**
2
+ * Integration test: full dApp ↔ wallet flow through MockRelay.
3
+ *
4
+ * Verifies the complete lifecycle:
5
+ * 1. DApp creates pairing
6
+ * 2. Wallet joins via URI
7
+ * 3. Pairing codes match
8
+ * 4. DApp accepts → both connected
9
+ * 5. DApp sends request → wallet receives → wallet approves → dApp gets response
10
+ * 6. Wallet pushes event → dApp receives
11
+ * 7. Close
12
+ */
13
+
14
+ import { describe, it, expect, vi } from 'vitest';
15
+ import { DAppSession } from './dapp-session.js';
16
+ import { WalletSession } from './wallet-session.js';
17
+ import { MockTransport, MockRelay, parseSnapshot } from './test-helpers.js';
18
+ import { parsePairingUri } from './crypto.js';
19
+
20
+ function wait(ms = 50): Promise<void> {
21
+ return new Promise((r) => setTimeout(r, ms));
22
+ }
23
+
24
+ describe('Integration: DApp ↔ Wallet full flow', () => {
25
+ it('completes full pairing and request/response cycle', async () => {
26
+ // Setup transports + relay
27
+ const dappTransport = new MockTransport();
28
+ const walletTransport = new MockTransport();
29
+ const _relay = new MockRelay(dappTransport, walletTransport);
30
+
31
+ // Create sessions
32
+ const dappSession = new DAppSession({
33
+ transport: dappTransport,
34
+ meta: { name: 'Test dApp', description: 'Test', url: 'https://test.com', icon: 'https://test.com/icon.png' },
35
+ });
36
+
37
+ const walletSession = new WalletSession({
38
+ transport: walletTransport,
39
+ capabilities: {
40
+ methods: ['wallet_getAccounts', 'wallet_signMessage'],
41
+ events: ['accountsChanged'],
42
+ chains: ['eip155:1'],
43
+ },
44
+ meta: { name: 'Test Wallet', description: 'Test', url: 'https://test.com', icon: 'https://test.com/icon.png', address: '0xWalletAddr' },
45
+ });
46
+
47
+ // Track events
48
+ const dappPhases: string[] = [];
49
+ const walletPhases: string[] = [];
50
+ dappSession.on('phase', (p) => dappPhases.push(p));
51
+ walletSession.on('phase', (p) => walletPhases.push(p));
52
+
53
+ // Step 1: DApp creates pairing
54
+ const pairingUri = await dappSession.createPairing();
55
+ expect(pairingUri).toContain('walletpair:?ch=');
56
+ await wait();
57
+ expect(dappSession.phase).toBe('waiting');
58
+
59
+ // Step 2: Wallet joins
60
+ // Need to set the walletTransport URL from the pairing URI
61
+ const parsed = parsePairingUri(pairingUri);
62
+ if ('setUrl' in walletTransport) {
63
+ (walletTransport as any).setUrl = () => {}; // mock
64
+ }
65
+
66
+ const sessionFingerprint = await walletSession.joinFromUri(pairingUri);
67
+ await wait();
68
+
69
+ expect(sessionFingerprint).toMatch(/^\d{4}$/);
70
+
71
+ // Step 3: Session fingerprints match
72
+ expect(dappSession.sessionFingerprint).toBe(walletSession.sessionFingerprint);
73
+
74
+ // Verify wallet capabilities were received
75
+ expect(dappSession.walletCapabilities?.methods).toContain('wallet_getAccounts');
76
+ expect(dappSession.walletMeta?.name).toBe('Test Wallet');
77
+
78
+ // Step 4: DApp auto-accepts (no manual acceptWallet needed)
79
+ await wait();
80
+
81
+ expect(dappSession.phase).toBe('connected');
82
+ expect(walletSession.phase).toBe('connected');
83
+
84
+ // Step 5: DApp sends request → wallet responds
85
+ walletSession.on('request', ({ id, method, params }) => {
86
+ if (method === 'wallet_getAccounts') {
87
+ walletSession.approve(id, ['0xWalletAddr']);
88
+ }
89
+ });
90
+
91
+ const accounts = await dappSession.request('wallet_getAccounts');
92
+ expect(accounts).toEqual(['0xWalletAddr']);
93
+
94
+ // Step 6: Wallet pushes event → dApp receives
95
+ const eventHandler = vi.fn();
96
+ dappSession.on('event', eventHandler);
97
+
98
+ walletSession.pushEvent('accountsChanged', { accounts: ['0xNewAddr'] });
99
+ await wait();
100
+
101
+ expect(eventHandler).toHaveBeenCalledWith({
102
+ event: 'accountsChanged',
103
+ data: { accounts: ['0xNewAddr'] },
104
+ });
105
+
106
+ // Step 7: Close
107
+ dappSession.close();
108
+ expect(dappSession.phase).toBe('closed');
109
+
110
+ // Verify phase transitions
111
+ expect(dappPhases).toContain('waiting');
112
+ expect(dappPhases).toContain('connected');
113
+ expect(dappPhases).toContain('closed');
114
+
115
+ expect(walletPhases).toContain('waiting_accept');
116
+ expect(walletPhases).toContain('connected');
117
+ });
118
+
119
+ it('wallet rejects request', async () => {
120
+ const dappTransport = new MockTransport();
121
+ const walletTransport = new MockTransport();
122
+ const _relay = new MockRelay(dappTransport, walletTransport);
123
+
124
+ const dappSession = new DAppSession({ transport: dappTransport, meta: { name: 'Test dApp', description: 'Test', url: 'https://test.com', icon: 'https://test.com/icon.png' } });
125
+ const walletSession = new WalletSession({
126
+ transport: walletTransport,
127
+ capabilities: { methods: ['wallet_signMessage'], events: [], chains: ['eip155:1'] },
128
+ meta: { name: 'Test Wallet', description: 'Test', url: 'https://test.com', icon: 'https://test.com/icon.png' },
129
+ });
130
+
131
+ // Connect (auto-accept)
132
+ const uri = await dappSession.createPairing();
133
+ await walletSession.joinFromUri(uri);
134
+ await wait();
135
+ await wait();
136
+
137
+ // Wallet rejects
138
+ walletSession.on('request', ({ id }) => {
139
+ walletSession.reject(id, 'user_rejected', 'No thanks');
140
+ });
141
+
142
+ await expect(dappSession.request('wallet_signMessage', { message: 'hi' }))
143
+ .rejects.toThrow('No thanks');
144
+ });
145
+
146
+ it('multiple sequential requests', async () => {
147
+ const dappTransport = new MockTransport();
148
+ const walletTransport = new MockTransport();
149
+ const _relay = new MockRelay(dappTransport, walletTransport);
150
+
151
+ const dappSession = new DAppSession({ transport: dappTransport, meta: { name: 'Test dApp', description: 'Test', url: 'https://test.com', icon: 'https://test.com/icon.png' } });
152
+ const walletSession = new WalletSession({
153
+ transport: walletTransport,
154
+ capabilities: { methods: ['wallet_getAccounts'], events: [], chains: ['eip155:1'] },
155
+ meta: { name: 'Test Wallet', description: 'Test', url: 'https://test.com', icon: 'https://test.com/icon.png' },
156
+ });
157
+
158
+ const uri = await dappSession.createPairing();
159
+ await walletSession.joinFromUri(uri);
160
+ await wait();
161
+ await wait();
162
+
163
+ let callCount = 0;
164
+ walletSession.on('request', ({ id, method }) => {
165
+ callCount++;
166
+ walletSession.approve(id, { call: callCount });
167
+ });
168
+
169
+ const r1 = await dappSession.request('wallet_getAccounts');
170
+ const r2 = await dappSession.request('wallet_getAccounts');
171
+ const r3 = await dappSession.request('wallet_getAccounts');
172
+
173
+ expect(r1).toEqual({ call: 1 });
174
+ expect(r2).toEqual({ call: 2 });
175
+ expect(r3).toEqual({ call: 3 });
176
+ });
177
+ });
178
+
179
+ // ---------------------------------------------------------------------------
180
+ // Directional key integration tests
181
+ // ---------------------------------------------------------------------------
182
+
183
+ describe('Integration: Bidirectional flow with directional keys', () => {
184
+
185
+ it('full bidirectional flow: dApp sends request, wallet receives, wallet responds, dApp receives', async () => {
186
+ const dappTransport = new MockTransport();
187
+ const walletTransport = new MockTransport();
188
+ const _relay = new MockRelay(dappTransport, walletTransport);
189
+
190
+ const dappSession = new DAppSession({ transport: dappTransport, meta: { name: 'BiDi dApp', description: 'Test', url: 'https://test.com', icon: 'https://test.com/icon.png' } });
191
+ const walletSession = new WalletSession({
192
+ transport: walletTransport,
193
+ capabilities: { methods: ['wallet_getAccounts', 'wallet_signMessage'], events: ['accountsChanged'], chains: ['eip155:1'] },
194
+ meta: { name: 'BiDi Wallet', description: 'Test', url: 'https://test.com', icon: 'https://test.com/icon.png', address: '0xBiDi' },
195
+ });
196
+
197
+ const uri = await dappSession.createPairing();
198
+ await walletSession.joinFromUri(uri);
199
+ await wait();
200
+
201
+ // Verify session fingerprints match
202
+ expect(dappSession.sessionFingerprint).toBe(walletSession.sessionFingerprint);
203
+
204
+ await wait();
205
+
206
+ expect(dappSession.phase).toBe('connected');
207
+ expect(walletSession.phase).toBe('connected');
208
+
209
+ // Wallet handles requests
210
+ walletSession.on('request', ({ id, method, params }) => {
211
+ if (method === 'wallet_getAccounts') {
212
+ walletSession.approve(id, ['0xBiDi']);
213
+ } else if (method === 'wallet_signMessage') {
214
+ walletSession.approve(id, { signature: '0xSIG' });
215
+ }
216
+ });
217
+
218
+ // DApp sends request (uses dappToWalletKey), wallet decrypts (uses dappToWalletKey as recvKey)
219
+ const accounts = await dappSession.request('wallet_getAccounts');
220
+ expect(accounts).toEqual(['0xBiDi']);
221
+
222
+ // Wallet responds (uses walletToDappKey), dApp decrypts (uses walletToDappKey as recvKey)
223
+ const sig = await dappSession.request('wallet_signMessage', { message: 'test' });
224
+ expect(sig).toEqual({ signature: '0xSIG' });
225
+ });
226
+
227
+ it('wallet pushes event, dApp receives with correct key', async () => {
228
+ const dappTransport = new MockTransport();
229
+ const walletTransport = new MockTransport();
230
+ const _relay = new MockRelay(dappTransport, walletTransport);
231
+
232
+ const dappSession = new DAppSession({ transport: dappTransport, meta: { name: 'Test dApp', description: 'Test', url: 'https://test.com', icon: 'https://test.com/icon.png' } });
233
+ const walletSession = new WalletSession({
234
+ transport: walletTransport,
235
+ capabilities: { methods: ['wallet_getAccounts'], events: ['accountsChanged', 'chainChanged'], chains: ['eip155:1'] },
236
+ meta: { name: 'Test Wallet', description: 'Test', url: 'https://test.com', icon: 'https://test.com/icon.png' },
237
+ });
238
+
239
+ const uri = await dappSession.createPairing();
240
+ await walletSession.joinFromUri(uri);
241
+ await wait();
242
+ await wait();
243
+
244
+ const events: Array<{ event: string; data: unknown }> = [];
245
+ dappSession.on('event', (evt) => events.push(evt));
246
+
247
+ // Push multiple events
248
+ walletSession.pushEvent('accountsChanged', { accounts: ['0xNew'] });
249
+ walletSession.pushEvent('chainChanged', { chainId: 'eip155:137' });
250
+ await wait();
251
+
252
+ expect(events).toHaveLength(2);
253
+ expect(events[0]).toEqual({ event: 'accountsChanged', data: { accounts: ['0xNew'] } });
254
+ expect(events[1]).toEqual({ event: 'chainChanged', data: { chainId: 'eip155:137' } });
255
+ });
256
+
257
+ it('multiple concurrent requests do not interfere', async () => {
258
+ const dappTransport = new MockTransport();
259
+ const walletTransport = new MockTransport();
260
+ const _relay = new MockRelay(dappTransport, walletTransport);
261
+
262
+ const dappSession = new DAppSession({ transport: dappTransport, meta: { name: 'Test dApp', description: 'Test', url: 'https://test.com', icon: 'https://test.com/icon.png' } });
263
+ const walletSession = new WalletSession({
264
+ transport: walletTransport,
265
+ capabilities: { methods: ['wallet_getAccounts', 'wallet_signMessage'], events: [], chains: ['eip155:1'] },
266
+ meta: { name: 'Test Wallet', description: 'Test', url: 'https://test.com', icon: 'https://test.com/icon.png' },
267
+ });
268
+
269
+ const uri = await dappSession.createPairing();
270
+ await walletSession.joinFromUri(uri);
271
+ await wait();
272
+ await wait();
273
+
274
+ // Wallet responds to each request with method-specific data, but with a delay
275
+ walletSession.on('request', ({ id, method }) => {
276
+ if (method === 'wallet_getAccounts') {
277
+ // Respond immediately
278
+ walletSession.approve(id, ['0xABC']);
279
+ } else if (method === 'wallet_signMessage') {
280
+ // Respond slightly delayed
281
+ setTimeout(() => walletSession.approve(id, '0xSIG123'), 10);
282
+ }
283
+ });
284
+
285
+ // Fire both concurrently
286
+ const [accounts, signature] = await Promise.all([
287
+ dappSession.request('wallet_getAccounts'),
288
+ dappSession.request('wallet_signMessage', { message: 'hello' }),
289
+ ]);
290
+
291
+ expect(accounts).toEqual(['0xABC']);
292
+ expect(signature).toBe('0xSIG123');
293
+ });
294
+
295
+ it('session serialization preserves directional keys', async () => {
296
+ const dappTransport = new MockTransport();
297
+ const walletTransport = new MockTransport();
298
+ const _relay = new MockRelay(dappTransport, walletTransport);
299
+
300
+ const dappSession = new DAppSession({ transport: dappTransport, meta: { name: 'Persist dApp', description: 'Test', url: 'https://test.com', icon: 'https://test.com/icon.png' } });
301
+ const walletSession = new WalletSession({
302
+ transport: walletTransport,
303
+ capabilities: { methods: ['wallet_getAccounts'], events: [], chains: ['eip155:1'] },
304
+ meta: { name: 'Persist Wallet', description: 'Test', url: 'https://test.com', icon: 'https://test.com/icon.png' },
305
+ });
306
+
307
+ const uri = await dappSession.createPairing();
308
+ await walletSession.joinFromUri(uri);
309
+ await wait();
310
+ await wait();
311
+
312
+ // Serialize both sessions
313
+ const dappJson = dappSession.serialize();
314
+ const walletJson = walletSession.serialize();
315
+
316
+ // Parse and verify directional keys are stored
317
+ const dappState = parseSnapshot(dappJson);
318
+ const walletState = parseSnapshot(walletJson);
319
+
320
+ expect(dappState.sendKey).toBeTruthy();
321
+ expect(dappState.recvKey).toBeTruthy();
322
+ expect(walletState.sendKey).toBeTruthy();
323
+ expect(walletState.recvKey).toBeTruthy();
324
+
325
+ // DApp sendKey (dappToWalletKey) == Wallet recvKey (dappToWalletKey)
326
+ expect(dappState.sendKey).toBe(walletState.recvKey);
327
+ // DApp recvKey (walletToDappKey) == Wallet sendKey (walletToDappKey)
328
+ expect(dappState.recvKey).toBe(walletState.sendKey);
329
+
330
+ // sendKey != recvKey (directional)
331
+ expect(dappState.sendKey).not.toBe(dappState.recvKey);
332
+ });
333
+
334
+ it('restored session can still communicate', async () => {
335
+ const dappTransport = new MockTransport();
336
+ const walletTransport = new MockTransport();
337
+ const _relay = new MockRelay(dappTransport, walletTransport);
338
+
339
+ const dappSession = new DAppSession({ transport: dappTransport, meta: { name: 'Restore dApp', description: 'Test', url: 'https://test.com', icon: 'https://test.com/icon.png' } });
340
+ const walletSession = new WalletSession({
341
+ transport: walletTransport,
342
+ capabilities: { methods: ['wallet_getAccounts'], events: [], chains: ['eip155:1'] },
343
+ meta: { name: 'Restore Wallet', description: 'Test', url: 'https://test.com', icon: 'https://test.com/icon.png' },
344
+ });
345
+
346
+ const uri = await dappSession.createPairing();
347
+ await walletSession.joinFromUri(uri);
348
+ await wait();
349
+ await wait();
350
+
351
+ // Exchange one message to advance sequence counters
352
+ walletSession.on('request', ({ id }) => walletSession.approve(id, 'first'));
353
+ const r1 = await dappSession.request('wallet_getAccounts');
354
+ expect(r1).toBe('first');
355
+
356
+ // Serialize
357
+ const dappJson = dappSession.serialize();
358
+ const walletJson = walletSession.serialize();
359
+
360
+ // Restore dApp session to a new transport that is linked to the wallet transport
361
+ const newDappTransport = new MockTransport();
362
+ const newWalletTransport = new MockTransport();
363
+ newDappTransport.peer = newWalletTransport;
364
+ newWalletTransport.peer = newDappTransport;
365
+
366
+ const restoredDapp = new DAppSession({ transport: newDappTransport, meta: { name: 'Restore dApp', description: 'Test', url: 'https://test.com', icon: 'https://test.com/icon.png' } });
367
+ expect(restoredDapp.restore(dappJson)).toBe(true);
368
+ (restoredDapp as any).phase = 'connected';
369
+
370
+ const restoredWallet = new WalletSession({
371
+ transport: newWalletTransport,
372
+ capabilities: { methods: ['wallet_getAccounts'], events: [], chains: ['eip155:1'] },
373
+ meta: { name: 'Restore Wallet', description: 'Test', url: 'https://test.com', icon: 'https://test.com/icon.png' },
374
+ });
375
+ expect(restoredWallet.restore(walletJson)).toBe(true);
376
+ (restoredWallet as any).phase = 'connected';
377
+
378
+ // Set up request handler on restored wallet
379
+ restoredWallet.on('request', ({ id }) => restoredWallet.approve(id, 'restored'));
380
+
381
+ // Restored dApp sends request through linked transports
382
+ const r2 = await restoredDapp.request('wallet_getAccounts');
383
+ expect(r2).toBe('restored');
384
+ });
385
+ });