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.
- package/LICENSE +21 -0
- package/README.md +415 -0
- package/dist/ble/framing.d.ts +23 -0
- package/dist/ble/framing.d.ts.map +1 -0
- package/dist/ble/framing.js +83 -0
- package/dist/ble/framing.js.map +1 -0
- package/dist/ble/index.d.ts +9 -0
- package/dist/ble/index.d.ts.map +1 -0
- package/dist/ble/index.js +9 -0
- package/dist/ble/index.js.map +1 -0
- package/dist/ble/web-ble-transport.d.ts +29 -0
- package/dist/ble/web-ble-transport.d.ts.map +1 -0
- package/dist/ble/web-ble-transport.js +93 -0
- package/dist/ble/web-ble-transport.js.map +1 -0
- package/dist/crypto.d.ts +102 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/crypto.js +279 -0
- package/dist/crypto.js.map +1 -0
- package/dist/dapp-session.d.ts +106 -0
- package/dist/dapp-session.d.ts.map +1 -0
- package/dist/dapp-session.js +918 -0
- package/dist/dapp-session.js.map +1 -0
- package/dist/emitter.d.ts +16 -0
- package/dist/emitter.d.ts.map +1 -0
- package/dist/emitter.js +41 -0
- package/dist/emitter.js.map +1 -0
- package/dist/evm/eip1193.d.ts +83 -0
- package/dist/evm/eip1193.d.ts.map +1 -0
- package/dist/evm/eip1193.js +270 -0
- package/dist/evm/eip1193.js.map +1 -0
- package/dist/evm/index.d.ts +8 -0
- package/dist/evm/index.d.ts.map +1 -0
- package/dist/evm/index.js +8 -0
- package/dist/evm/index.js.map +1 -0
- package/dist/evm/wagmi.d.ts +118 -0
- package/dist/evm/wagmi.d.ts.map +1 -0
- package/dist/evm/wagmi.js +205 -0
- package/dist/evm/wagmi.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +225 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +31 -0
- package/dist/types.js.map +1 -0
- package/dist/wallet-session.d.ts +107 -0
- package/dist/wallet-session.d.ts.map +1 -0
- package/dist/wallet-session.js +794 -0
- package/dist/wallet-session.js.map +1 -0
- package/dist/ws-transport.d.ts +29 -0
- package/dist/ws-transport.d.ts.map +1 -0
- package/dist/ws-transport.js +79 -0
- package/dist/ws-transport.js.map +1 -0
- package/package.json +55 -0
- package/src/__tests__/adversarial/crypto-attacks.test.ts +557 -0
- package/src/__tests__/adversarial/malicious-dapp.test.ts +505 -0
- package/src/__tests__/adversarial/malicious-relay.test.ts +528 -0
- package/src/__tests__/adversarial/malicious-wallet.test.ts +467 -0
- package/src/__tests__/spec-compliance/canonical-json.test.ts +227 -0
- package/src/__tests__/spec-compliance/crypto-vectors.test.ts +321 -0
- package/src/__tests__/spec-compliance/message-format.test.ts +356 -0
- package/src/__tests__/spec-compliance/sequence-numbers.test.ts +300 -0
- package/src/__tests__/spec-compliance/state-machine.test.ts +364 -0
- package/src/ble/framing.test.ts +196 -0
- package/src/ble/framing.ts +100 -0
- package/src/ble/index.ts +18 -0
- package/src/ble/web-ble-transport.test.ts +192 -0
- package/src/ble/web-ble-transport.ts +116 -0
- package/src/ble/web-bluetooth.d.ts +47 -0
- package/src/canonical-json.test.ts +612 -0
- package/src/crypto-directional.test.ts +263 -0
- package/src/crypto-hardening.test.ts +529 -0
- package/src/crypto.test.ts +635 -0
- package/src/crypto.ts +405 -0
- package/src/dapp-session.test.ts +647 -0
- package/src/dapp-session.ts +1004 -0
- package/src/emitter.test.ts +169 -0
- package/src/emitter.ts +45 -0
- package/src/evm/eip1193.test.ts +365 -0
- package/src/evm/eip1193.ts +346 -0
- package/src/evm/index.ts +19 -0
- package/src/evm/wagmi.test.ts +396 -0
- package/src/evm/wagmi.ts +321 -0
- package/src/index.ts +86 -0
- package/src/integration.test.ts +385 -0
- package/src/security.test.ts +430 -0
- package/src/sequence-validation.test.ts +1185 -0
- package/src/test-helpers.ts +216 -0
- package/src/types.test.ts +82 -0
- package/src/types.ts +305 -0
- package/src/wallet-session.test.ts +683 -0
- package/src/wallet-session.ts +922 -0
- package/src/ws-transport.test.ts +231 -0
- package/src/ws-transport.ts +92 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { WebSocketTransport } from './ws-transport.js';
|
|
3
|
+
|
|
4
|
+
// Mock WebSocket for Node.js environment
|
|
5
|
+
class MockWebSocket {
|
|
6
|
+
static instances: MockWebSocket[] = [];
|
|
7
|
+
|
|
8
|
+
url: string;
|
|
9
|
+
protocols: string[];
|
|
10
|
+
readyState = 0; // CONNECTING
|
|
11
|
+
onopen: (() => void) | null = null;
|
|
12
|
+
onmessage: ((event: { data: string }) => void) | null = null;
|
|
13
|
+
onclose: (() => void) | null = null;
|
|
14
|
+
onerror: (() => void) | null = null;
|
|
15
|
+
sentMessages: string[] = [];
|
|
16
|
+
|
|
17
|
+
constructor(url: string, protocols?: string[]) {
|
|
18
|
+
this.url = url;
|
|
19
|
+
this.protocols = protocols ?? [];
|
|
20
|
+
MockWebSocket.instances.push(this);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
send(data: string) {
|
|
24
|
+
this.sentMessages.push(data);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
close() {
|
|
28
|
+
this.readyState = 3; // CLOSED
|
|
29
|
+
this.onclose?.();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Test helpers
|
|
33
|
+
simulateOpen() {
|
|
34
|
+
this.readyState = 1; // OPEN
|
|
35
|
+
this.onopen?.();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
simulateMessage(data: string) {
|
|
39
|
+
this.onmessage?.({ data });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
simulateClose() {
|
|
43
|
+
this.readyState = 3;
|
|
44
|
+
this.onclose?.();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
simulateError() {
|
|
48
|
+
this.onerror?.();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe('WebSocketTransport', () => {
|
|
53
|
+
let originalWebSocket: typeof globalThis.WebSocket;
|
|
54
|
+
|
|
55
|
+
beforeEach(() => {
|
|
56
|
+
MockWebSocket.instances = [];
|
|
57
|
+
originalWebSocket = globalThis.WebSocket;
|
|
58
|
+
(globalThis as any).WebSocket = MockWebSocket;
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
afterEach(() => {
|
|
62
|
+
globalThis.WebSocket = originalWebSocket;
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('starts disconnected', () => {
|
|
66
|
+
const t = new WebSocketTransport('ws://localhost:8080/v1');
|
|
67
|
+
expect(t.state).toBe('disconnected');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('accepts string URL constructor', () => {
|
|
71
|
+
const t = new WebSocketTransport('ws://localhost:8080/v1');
|
|
72
|
+
expect(t.state).toBe('disconnected');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('accepts options object constructor', () => {
|
|
76
|
+
const t = new WebSocketTransport({ url: 'ws://localhost:8080/v1', protocols: ['custom'] });
|
|
77
|
+
expect(t.state).toBe('disconnected');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('connect', () => {
|
|
81
|
+
it('resolves on successful connection', async () => {
|
|
82
|
+
const t = new WebSocketTransport('ws://localhost:8080/v1');
|
|
83
|
+
|
|
84
|
+
const connectPromise = t.connect();
|
|
85
|
+
|
|
86
|
+
// Simulate WebSocket open
|
|
87
|
+
const ws = MockWebSocket.instances[0]!;
|
|
88
|
+
expect(ws.url).toBe('ws://localhost:8080/v1');
|
|
89
|
+
expect(ws.protocols).toEqual(['walletpair.v1']);
|
|
90
|
+
|
|
91
|
+
ws.simulateOpen();
|
|
92
|
+
|
|
93
|
+
await connectPromise;
|
|
94
|
+
expect(t.state).toBe('connected');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('rejects on connection failure', async () => {
|
|
98
|
+
const t = new WebSocketTransport('ws://localhost:8080/v1');
|
|
99
|
+
|
|
100
|
+
const connectPromise = t.connect();
|
|
101
|
+
|
|
102
|
+
const ws = MockWebSocket.instances[0]!;
|
|
103
|
+
ws.simulateError();
|
|
104
|
+
ws.simulateClose();
|
|
105
|
+
|
|
106
|
+
await expect(connectPromise).rejects.toThrow('WebSocket connection failed');
|
|
107
|
+
expect(t.state).toBe('disconnected');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('calls onOpen handler on successful connection', async () => {
|
|
111
|
+
const t = new WebSocketTransport('ws://localhost:8080/v1');
|
|
112
|
+
const openHandler = vi.fn();
|
|
113
|
+
t.onOpen(openHandler);
|
|
114
|
+
|
|
115
|
+
const promise = t.connect();
|
|
116
|
+
MockWebSocket.instances[0]!.simulateOpen();
|
|
117
|
+
await promise;
|
|
118
|
+
|
|
119
|
+
expect(openHandler).toHaveBeenCalledTimes(1);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe('send', () => {
|
|
124
|
+
it('sends JSON-serialized message', async () => {
|
|
125
|
+
const t = new WebSocketTransport('ws://localhost:8080/v1');
|
|
126
|
+
const promise = t.connect();
|
|
127
|
+
MockWebSocket.instances[0]!.simulateOpen();
|
|
128
|
+
await promise;
|
|
129
|
+
|
|
130
|
+
const msg = { v: 1, t: 'ping', ch: 'abc', ts: 123 } as any;
|
|
131
|
+
t.send(msg);
|
|
132
|
+
|
|
133
|
+
const ws = MockWebSocket.instances[0]!;
|
|
134
|
+
expect(ws.sentMessages).toHaveLength(1);
|
|
135
|
+
expect(JSON.parse(ws.sentMessages[0]!)).toEqual(msg);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('does nothing when not connected', () => {
|
|
139
|
+
const t = new WebSocketTransport('ws://localhost:8080/v1');
|
|
140
|
+
// Not connected, should not throw
|
|
141
|
+
t.send({ v: 1, t: 'ping', ch: 'abc', ts: 123 } as any);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe('receive', () => {
|
|
146
|
+
it('calls message handler on incoming messages', async () => {
|
|
147
|
+
const t = new WebSocketTransport('ws://localhost:8080/v1');
|
|
148
|
+
const handler = vi.fn();
|
|
149
|
+
t.onMessage(handler);
|
|
150
|
+
|
|
151
|
+
const promise = t.connect();
|
|
152
|
+
MockWebSocket.instances[0]!.simulateOpen();
|
|
153
|
+
await promise;
|
|
154
|
+
|
|
155
|
+
const msg = { v: 1, t: 'ready', ch: 'abc', state: 'waiting' };
|
|
156
|
+
MockWebSocket.instances[0]!.simulateMessage(JSON.stringify(msg));
|
|
157
|
+
|
|
158
|
+
expect(handler).toHaveBeenCalledWith(msg);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('ignores malformed JSON', async () => {
|
|
162
|
+
const t = new WebSocketTransport('ws://localhost:8080/v1');
|
|
163
|
+
const handler = vi.fn();
|
|
164
|
+
t.onMessage(handler);
|
|
165
|
+
|
|
166
|
+
const promise = t.connect();
|
|
167
|
+
MockWebSocket.instances[0]!.simulateOpen();
|
|
168
|
+
await promise;
|
|
169
|
+
|
|
170
|
+
MockWebSocket.instances[0]!.simulateMessage('not json');
|
|
171
|
+
expect(handler).not.toHaveBeenCalled();
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe('disconnect', () => {
|
|
176
|
+
it('transitions to disconnected', async () => {
|
|
177
|
+
const t = new WebSocketTransport('ws://localhost:8080/v1');
|
|
178
|
+
const promise = t.connect();
|
|
179
|
+
MockWebSocket.instances[0]!.simulateOpen();
|
|
180
|
+
await promise;
|
|
181
|
+
|
|
182
|
+
t.disconnect();
|
|
183
|
+
expect(t.state).toBe('disconnected');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('does not call close handler on intentional disconnect', async () => {
|
|
187
|
+
const t = new WebSocketTransport('ws://localhost:8080/v1');
|
|
188
|
+
const closeHandler = vi.fn();
|
|
189
|
+
t.onClose(closeHandler);
|
|
190
|
+
|
|
191
|
+
const promise = t.connect();
|
|
192
|
+
MockWebSocket.instances[0]!.simulateOpen();
|
|
193
|
+
await promise;
|
|
194
|
+
|
|
195
|
+
t.disconnect();
|
|
196
|
+
expect(closeHandler).not.toHaveBeenCalled();
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
describe('unexpected close', () => {
|
|
201
|
+
it('calls close handler on unexpected transport close', async () => {
|
|
202
|
+
const t = new WebSocketTransport('ws://localhost:8080/v1');
|
|
203
|
+
const closeHandler = vi.fn();
|
|
204
|
+
t.onClose(closeHandler);
|
|
205
|
+
|
|
206
|
+
const promise = t.connect();
|
|
207
|
+
MockWebSocket.instances[0]!.simulateOpen();
|
|
208
|
+
await promise;
|
|
209
|
+
|
|
210
|
+
// Simulate unexpected close (e.g., network drop)
|
|
211
|
+
MockWebSocket.instances[0]!.simulateClose();
|
|
212
|
+
|
|
213
|
+
expect(closeHandler).toHaveBeenCalledTimes(1);
|
|
214
|
+
expect(t.state).toBe('disconnected');
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
describe('setUrl', () => {
|
|
219
|
+
it('updates the URL for next connection', async () => {
|
|
220
|
+
const t = new WebSocketTransport('ws://localhost:8080/v1');
|
|
221
|
+
t.setUrl('ws://other:9090/v1');
|
|
222
|
+
|
|
223
|
+
const promise = t.connect();
|
|
224
|
+
const ws = MockWebSocket.instances[0]!;
|
|
225
|
+
expect(ws.url).toBe('ws://other:9090/v1');
|
|
226
|
+
|
|
227
|
+
ws.simulateOpen();
|
|
228
|
+
await promise;
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket transport for WalletPair protocol.
|
|
3
|
+
*
|
|
4
|
+
* Works in browsers, Node.js 22+, Deno, Bun — anything with a global WebSocket.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Transport, TransportState, ProtocolMessage } from './types.js';
|
|
8
|
+
|
|
9
|
+
export interface WebSocketTransportOptions {
|
|
10
|
+
url: string;
|
|
11
|
+
protocols?: string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class WebSocketTransport implements Transport {
|
|
15
|
+
state: TransportState = 'disconnected';
|
|
16
|
+
|
|
17
|
+
private ws: WebSocket | null = null;
|
|
18
|
+
private url: string;
|
|
19
|
+
private protocols: string[];
|
|
20
|
+
|
|
21
|
+
private messageHandler: ((msg: ProtocolMessage) => void) | null = null;
|
|
22
|
+
private closeHandler: (() => void) | null = null;
|
|
23
|
+
private openHandler: (() => void) | null = null;
|
|
24
|
+
|
|
25
|
+
constructor(options: WebSocketTransportOptions | string) {
|
|
26
|
+
if (typeof options === 'string') {
|
|
27
|
+
this.url = options;
|
|
28
|
+
this.protocols = ['walletpair.v1'];
|
|
29
|
+
} else {
|
|
30
|
+
this.url = options.url;
|
|
31
|
+
this.protocols = options.protocols ?? ['walletpair.v1'];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
onMessage(handler: (msg: ProtocolMessage) => void): void { this.messageHandler = handler; }
|
|
36
|
+
onClose(handler: () => void): void { this.closeHandler = handler; }
|
|
37
|
+
onOpen(handler: () => void): void { this.openHandler = handler; }
|
|
38
|
+
|
|
39
|
+
/** Update the relay URL (useful for reconnect to a different relay). */
|
|
40
|
+
setUrl(url: string): void {
|
|
41
|
+
this.url = url;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
connect(): Promise<void> {
|
|
45
|
+
return new Promise<void>((resolve, reject) => {
|
|
46
|
+
this.state = 'connecting';
|
|
47
|
+
const ws = new WebSocket(this.url, this.protocols);
|
|
48
|
+
|
|
49
|
+
ws.onopen = () => {
|
|
50
|
+
this.state = 'connected';
|
|
51
|
+
this.ws = ws;
|
|
52
|
+
this.openHandler?.();
|
|
53
|
+
resolve();
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
ws.onmessage = (event: MessageEvent) => {
|
|
57
|
+
if (this.messageHandler) {
|
|
58
|
+
try { this.messageHandler(JSON.parse(event.data as string)); } catch { /* bad json */ }
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
ws.onclose = () => {
|
|
63
|
+
const wasConnected = this.state === 'connected';
|
|
64
|
+
this.state = 'disconnected';
|
|
65
|
+
this.ws = null;
|
|
66
|
+
if (wasConnected) {
|
|
67
|
+
this.closeHandler?.();
|
|
68
|
+
} else {
|
|
69
|
+
reject(new Error('WebSocket connection failed'));
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
ws.onerror = () => {
|
|
74
|
+
// onclose will fire after onerror, which handles the reject
|
|
75
|
+
};
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
send(msg: ProtocolMessage): void {
|
|
80
|
+
if (!this.ws || this.state !== 'connected') return;
|
|
81
|
+
this.ws.send(JSON.stringify(msg));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
disconnect(): void {
|
|
85
|
+
if (this.ws) {
|
|
86
|
+
this.ws.onclose = null;
|
|
87
|
+
this.ws.close();
|
|
88
|
+
this.ws = null;
|
|
89
|
+
}
|
|
90
|
+
this.state = 'disconnected';
|
|
91
|
+
}
|
|
92
|
+
}
|